bumble 0.0.223__py3-none-any.whl → 0.0.224__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/host.py CHANGED
@@ -21,13 +21,16 @@ import asyncio
21
21
  import collections
22
22
  import dataclasses
23
23
  import logging
24
- import struct
25
24
  from collections.abc import Awaitable, Callable
26
- from typing import TYPE_CHECKING, Any, cast
25
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
27
26
 
28
27
  from bumble import drivers, hci, utils
29
28
  from bumble.colors import color
30
- from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
29
+ from bumble.core import (
30
+ ConnectionPHY,
31
+ InvalidStateError,
32
+ PhysicalTransport,
33
+ )
31
34
  from bumble.l2cap import L2CAP_PDU
32
35
  from bumble.snoop import Snooper
33
36
  from bumble.transport.common import TransportLostError
@@ -35,7 +38,6 @@ from bumble.transport.common import TransportLostError
35
38
  if TYPE_CHECKING:
36
39
  from bumble.transport.common import TransportSink, TransportSource
37
40
 
38
-
39
41
  # -----------------------------------------------------------------------------
40
42
  # Logging
41
43
  # -----------------------------------------------------------------------------
@@ -236,6 +238,9 @@ class IsoLink:
236
238
 
237
239
 
238
240
  # -----------------------------------------------------------------------------
241
+ _RP = TypeVar('_RP', bound=hci.HCI_ReturnParameters)
242
+
243
+
239
244
  class Host(utils.EventEmitter):
240
245
  connections: dict[int, Connection]
241
246
  cis_links: dict[int, IsoLink]
@@ -264,13 +269,20 @@ class Host(utils.EventEmitter):
264
269
  self.bis_links = {} # BIS links, by connection handle
265
270
  self.sco_links = {} # SCO links, by connection handle
266
271
  self.bigs = {} # BIG Handle to BIS Handles
267
- self.pending_command = None
268
- self.pending_response: asyncio.Future[Any] | None = None
272
+ self.pending_command: hci.HCI_SyncCommand | hci.HCI_AsyncCommand | None = None
273
+ self.pending_response: (
274
+ asyncio.Future[
275
+ hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event
276
+ ]
277
+ | None
278
+ ) = None
269
279
  self.number_of_supported_advertising_sets = 0
270
280
  self.maximum_advertising_data_length = 31
271
- self.local_version = None
281
+ self.local_version: (
282
+ hci.HCI_Read_Local_Version_Information_ReturnParameters | None
283
+ ) = None
272
284
  self.local_supported_commands = 0
273
- self.local_le_features = 0
285
+ self.local_le_features = hci.LeFeatureMask(0) # LE features
274
286
  self.local_lmp_features = hci.LmpFeatureMask(0) # Classic LMP features
275
287
  self.suggested_max_tx_octets = 251 # Max allowed
276
288
  self.suggested_max_tx_time = 2120 # Max allowed
@@ -312,7 +324,7 @@ class Host(utils.EventEmitter):
312
324
  self.emit('flush')
313
325
  self.command_semaphore.release()
314
326
 
315
- async def reset(self, driver_factory=drivers.get_driver_for_host):
327
+ async def reset(self, driver_factory=drivers.get_driver_for_host) -> None:
316
328
  if self.ready:
317
329
  self.ready = False
318
330
  await self.flush()
@@ -330,57 +342,61 @@ class Host(utils.EventEmitter):
330
342
 
331
343
  # Send a reset command unless a driver has already done so.
332
344
  if reset_needed:
333
- await self.send_command(hci.HCI_Reset_Command(), check_result=True)
345
+ await self.send_sync_command(hci.HCI_Reset_Command())
334
346
  self.ready = True
335
347
 
336
- response = await self.send_command(
337
- hci.HCI_Read_Local_Supported_Commands_Command(), check_result=True
348
+ response1 = await self.send_sync_command(
349
+ hci.HCI_Read_Local_Supported_Commands_Command()
338
350
  )
339
351
  self.local_supported_commands = int.from_bytes(
340
- response.return_parameters.supported_commands, 'little'
352
+ response1.supported_commands, 'little'
341
353
  )
342
354
 
343
- if self.supports_command(hci.HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
344
- response = await self.send_command(
345
- hci.HCI_LE_Read_Local_Supported_Features_Command(), check_result=True
355
+ if self.supports_command(hci.HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND):
356
+ self.local_version = await self.send_sync_command(
357
+ hci.HCI_Read_Local_Version_Information_Command()
346
358
  )
347
- self.local_le_features = struct.unpack(
348
- '<Q', response.return_parameters.le_features
349
- )[0]
350
359
 
351
- if self.supports_command(hci.HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND):
352
- response = await self.send_command(
353
- hci.HCI_Read_Local_Version_Information_Command(), check_result=True
360
+ if self.supports_command(hci.HCI_LE_READ_ALL_LOCAL_SUPPORTED_FEATURES_COMMAND):
361
+ response2 = await self.send_sync_command(
362
+ hci.HCI_LE_Read_All_Local_Supported_Features_Command()
363
+ )
364
+ self.local_le_features = hci.LeFeatureMask(
365
+ int.from_bytes(response2.le_features, 'little')
366
+ )
367
+ elif self.supports_command(hci.HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
368
+ response3 = await self.send_sync_command(
369
+ hci.HCI_LE_Read_Local_Supported_Features_Command()
370
+ )
371
+ self.local_le_features = hci.LeFeatureMask(
372
+ int.from_bytes(response3.le_features, 'little')
354
373
  )
355
- self.local_version = response.return_parameters
356
374
 
357
375
  if self.supports_command(hci.HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND):
358
376
  max_page_number = 0
359
377
  page_number = 0
360
378
  lmp_features = 0
361
379
  while page_number <= max_page_number:
362
- response = await self.send_command(
380
+ response4 = await self.send_sync_command(
363
381
  hci.HCI_Read_Local_Extended_Features_Command(
364
382
  page_number=page_number
365
- ),
366
- check_result=True,
383
+ )
367
384
  )
368
385
  lmp_features |= int.from_bytes(
369
- response.return_parameters.extended_lmp_features, 'little'
386
+ response4.extended_lmp_features, 'little'
370
387
  ) << (64 * page_number)
371
- max_page_number = response.return_parameters.maximum_page_number
388
+ max_page_number = response4.maximum_page_number
372
389
  page_number += 1
373
390
  self.local_lmp_features = hci.LmpFeatureMask(lmp_features)
374
-
375
391
  elif self.supports_command(hci.HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
376
- response = await self.send_command(
377
- hci.HCI_Read_Local_Supported_Features_Command(), check_result=True
392
+ response5 = await self.send_sync_command(
393
+ hci.HCI_Read_Local_Supported_Features_Command()
378
394
  )
379
395
  self.local_lmp_features = hci.LmpFeatureMask(
380
- int.from_bytes(response.return_parameters.lmp_features, 'little')
396
+ int.from_bytes(response5.lmp_features, 'little')
381
397
  )
382
398
 
383
- await self.send_command(
399
+ await self.send_sync_command(
384
400
  hci.HCI_Set_Event_Mask_Command(
385
401
  event_mask=hci.HCI_Set_Event_Mask_Command.mask(
386
402
  [
@@ -437,7 +453,7 @@ class Host(utils.EventEmitter):
437
453
  )
438
454
  )
439
455
  if self.supports_command(hci.HCI_SET_EVENT_MASK_PAGE_2_COMMAND):
440
- await self.send_command(
456
+ await self.send_sync_command(
441
457
  hci.HCI_Set_Event_Mask_Page_2_Command(
442
458
  event_mask_page_2=hci.HCI_Set_Event_Mask_Page_2_Command.mask(
443
459
  [hci.HCI_ENCRYPTION_CHANGE_V2_EVENT]
@@ -490,29 +506,28 @@ class Host(utils.EventEmitter):
490
506
  hci.HCI_LE_TRANSMIT_POWER_REPORTING_EVENT,
491
507
  hci.HCI_LE_BIGINFO_ADVERTISING_REPORT_EVENT,
492
508
  hci.HCI_LE_SUBRATE_CHANGE_EVENT,
509
+ hci.HCI_LE_READ_ALL_REMOTE_FEATURES_COMPLETE_EVENT,
493
510
  hci.HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT,
494
511
  hci.HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT,
495
512
  hci.HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT,
496
513
  hci.HCI_LE_CS_CONFIG_COMPLETE_EVENT,
497
514
  hci.HCI_LE_CS_SUBEVENT_RESULT_EVENT,
498
515
  hci.HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT,
516
+ hci.HCI_LE_MONITORED_ADVERTISERS_REPORT_EVENT,
517
+ hci.HCI_LE_FRAME_SPACE_UPDATE_COMPLETE_EVENT,
518
+ hci.HCI_LE_UTP_RECEIVE_EVENT,
519
+ hci.HCI_LE_CONNECTION_RATE_CHANGE_EVENT,
499
520
  ]
500
521
  )
501
522
 
502
- await self.send_command(
523
+ await self.send_sync_command(
503
524
  hci.HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
504
525
  )
505
526
 
506
527
  if self.supports_command(hci.HCI_READ_BUFFER_SIZE_COMMAND):
507
- response = await self.send_command(
508
- hci.HCI_Read_Buffer_Size_Command(), check_result=True
509
- )
510
- hc_acl_data_packet_length = (
511
- response.return_parameters.hc_acl_data_packet_length
512
- )
513
- hc_total_num_acl_data_packets = (
514
- response.return_parameters.hc_total_num_acl_data_packets
515
- )
528
+ response6 = await self.send_sync_command(hci.HCI_Read_Buffer_Size_Command())
529
+ hc_acl_data_packet_length = response6.hc_acl_data_packet_length
530
+ hc_total_num_acl_data_packets = response6.hc_total_num_acl_data_packets
516
531
 
517
532
  logger.debug(
518
533
  'HCI ACL flow control: '
@@ -531,19 +546,13 @@ class Host(utils.EventEmitter):
531
546
  iso_data_packet_length = 0
532
547
  total_num_iso_data_packets = 0
533
548
  if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
534
- response = await self.send_command(
535
- hci.HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
536
- )
537
- le_acl_data_packet_length = (
538
- response.return_parameters.le_acl_data_packet_length
539
- )
540
- total_num_le_acl_data_packets = (
541
- response.return_parameters.total_num_le_acl_data_packets
542
- )
543
- iso_data_packet_length = response.return_parameters.iso_data_packet_length
544
- total_num_iso_data_packets = (
545
- response.return_parameters.total_num_iso_data_packets
549
+ response7 = await self.send_sync_command(
550
+ hci.HCI_LE_Read_Buffer_Size_V2_Command()
546
551
  )
552
+ le_acl_data_packet_length = response7.le_acl_data_packet_length
553
+ total_num_le_acl_data_packets = response7.total_num_le_acl_data_packets
554
+ iso_data_packet_length = response7.iso_data_packet_length
555
+ total_num_iso_data_packets = response7.total_num_iso_data_packets
547
556
 
548
557
  logger.debug(
549
558
  'HCI LE flow control: '
@@ -553,15 +562,11 @@ class Host(utils.EventEmitter):
553
562
  f'total_num_iso_data_packets={total_num_iso_data_packets}'
554
563
  )
555
564
  elif self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
556
- response = await self.send_command(
557
- hci.HCI_LE_Read_Buffer_Size_Command(), check_result=True
558
- )
559
- le_acl_data_packet_length = (
560
- response.return_parameters.le_acl_data_packet_length
561
- )
562
- total_num_le_acl_data_packets = (
563
- response.return_parameters.total_num_le_acl_data_packets
565
+ response8 = await self.send_sync_command(
566
+ hci.HCI_LE_Read_Buffer_Size_Command()
564
567
  )
568
+ le_acl_data_packet_length = response8.le_acl_data_packet_length
569
+ total_num_le_acl_data_packets = response8.total_num_le_acl_data_packets
565
570
 
566
571
  logger.debug(
567
572
  'HCI LE ACL flow control: '
@@ -592,16 +597,16 @@ class Host(utils.EventEmitter):
592
597
  ) and self.supports_command(
593
598
  hci.HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
594
599
  ):
595
- response = await self.send_command(
600
+ response9 = await self.send_sync_command(
596
601
  hci.HCI_LE_Read_Suggested_Default_Data_Length_Command()
597
602
  )
598
- suggested_max_tx_octets = response.return_parameters.suggested_max_tx_octets
599
- suggested_max_tx_time = response.return_parameters.suggested_max_tx_time
603
+ suggested_max_tx_octets = response9.suggested_max_tx_octets
604
+ suggested_max_tx_time = response9.suggested_max_tx_time
600
605
  if (
601
606
  suggested_max_tx_octets != self.suggested_max_tx_octets
602
607
  or suggested_max_tx_time != self.suggested_max_tx_time
603
608
  ):
604
- await self.send_command(
609
+ await self.send_sync_command(
605
610
  hci.HCI_LE_Write_Suggested_Default_Data_Length_Command(
606
611
  suggested_max_tx_octets=self.suggested_max_tx_octets,
607
612
  suggested_max_tx_time=self.suggested_max_tx_time,
@@ -611,24 +616,28 @@ class Host(utils.EventEmitter):
611
616
  if self.supports_command(
612
617
  hci.HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND
613
618
  ):
614
- response = await self.send_command(
615
- hci.HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command(),
616
- check_result=True,
617
- )
618
- self.number_of_supported_advertising_sets = (
619
- response.return_parameters.num_supported_advertising_sets
620
- )
619
+ try:
620
+ response10 = await self.send_sync_command(
621
+ hci.HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
622
+ )
623
+ self.number_of_supported_advertising_sets = (
624
+ response10.num_supported_advertising_sets
625
+ )
626
+ except hci.HCI_Error:
627
+ logger.warning('Failed to read number of supported advertising sets')
621
628
 
622
629
  if self.supports_command(
623
630
  hci.HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND
624
631
  ):
625
- response = await self.send_command(
626
- hci.HCI_LE_Read_Maximum_Advertising_Data_Length_Command(),
627
- check_result=True,
628
- )
629
- self.maximum_advertising_data_length = (
630
- response.return_parameters.max_advertising_data_length
631
- )
632
+ try:
633
+ response11 = await self.send_sync_command(
634
+ hci.HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
635
+ )
636
+ self.maximum_advertising_data_length = (
637
+ response11.max_advertising_data_length
638
+ )
639
+ except hci.HCI_Error:
640
+ logger.warning('Failed to read maximum advertising data length')
632
641
 
633
642
  @property
634
643
  def controller(self) -> TransportSink | None:
@@ -654,56 +663,175 @@ class Host(utils.EventEmitter):
654
663
  if self.hci_sink:
655
664
  self.hci_sink.on_packet(bytes(packet))
656
665
 
657
- async def send_command(
658
- self, command, check_result=False, response_timeout: int | None = None
659
- ):
666
+ async def _send_command(
667
+ self,
668
+ command: hci.HCI_SyncCommand | hci.HCI_AsyncCommand,
669
+ response_timeout: float | None = None,
670
+ ) -> hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event:
660
671
  # Wait until we can send (only one pending command at a time)
661
- async with self.command_semaphore:
662
- assert self.pending_command is None
663
- assert self.pending_response is None
672
+ await self.command_semaphore.acquire()
664
673
 
665
- # Create a future value to hold the eventual response
666
- self.pending_response = asyncio.get_running_loop().create_future()
667
- self.pending_command = command
674
+ # Create a future value to hold the eventual response
675
+ assert self.pending_command is None
676
+ assert self.pending_response is None
677
+ self.pending_response = asyncio.get_running_loop().create_future()
678
+ self.pending_command = command
668
679
 
669
- try:
670
- self.send_hci_packet(command)
671
- await asyncio.wait_for(self.pending_response, timeout=response_timeout)
672
- response = self.pending_response.result()
673
-
674
- # Check the return parameters if required
675
- if check_result:
676
- if isinstance(response, hci.HCI_Command_Status_Event):
677
- status = response.status # type: ignore[attr-defined]
678
- elif isinstance(response.return_parameters, int):
679
- status = response.return_parameters
680
- elif isinstance(response.return_parameters, bytes):
681
- # return parameters first field is a one byte status code
682
- status = response.return_parameters[0]
683
- else:
684
- status = response.return_parameters.status
685
-
686
- if status != hci.HCI_SUCCESS:
687
- logger.warning(
688
- f'{command.name} failed '
689
- f'({hci.HCI_Constant.error_name(status)})'
690
- )
691
- raise hci.HCI_Error(status)
692
-
693
- return response
694
- except Exception:
695
- logger.exception(color("!!! Exception while sending command:", "red"))
696
- raise
697
- finally:
698
- self.pending_command = None
699
- self.pending_response = None
700
-
701
- # Use this method to send a command from a task
702
- def send_command_sync(self, command: hci.HCI_Command) -> None:
703
- async def send_command(command: hci.HCI_Command) -> None:
704
- await self.send_command(command)
705
-
706
- asyncio.create_task(send_command(command))
680
+ response: (
681
+ hci.HCI_Command_Complete_Event | hci.HCI_Command_Status_Event | None
682
+ ) = None
683
+ try:
684
+ self.send_hci_packet(command)
685
+ response = await asyncio.wait_for(
686
+ self.pending_response, timeout=response_timeout
687
+ )
688
+ return response
689
+ except Exception:
690
+ logger.exception(color("!!! Exception while sending command:", "red"))
691
+ raise
692
+ finally:
693
+ self.pending_command = None
694
+ self.pending_response = None
695
+ if (
696
+ response is not None
697
+ and response.num_hci_command_packets
698
+ and self.command_semaphore.locked()
699
+ ):
700
+ self.command_semaphore.release()
701
+
702
+ @overload
703
+ async def send_command(
704
+ self,
705
+ command: hci.HCI_SyncCommand[_RP],
706
+ check_result: bool = False,
707
+ response_timeout: float | None = None,
708
+ ) -> hci.HCI_Command_Complete_Event[_RP]: ...
709
+
710
+ @overload
711
+ async def send_command(
712
+ self,
713
+ command: hci.HCI_AsyncCommand,
714
+ check_result: bool = False,
715
+ response_timeout: float | None = None,
716
+ ) -> hci.HCI_Command_Status_Event: ...
717
+
718
+ async def send_command(
719
+ self,
720
+ command: hci.HCI_SyncCommand[_RP] | hci.HCI_AsyncCommand,
721
+ check_result: bool = False,
722
+ response_timeout: float | None = None,
723
+ ) -> hci.HCI_Command_Complete_Event[_RP] | hci.HCI_Command_Status_Event:
724
+ response = await self._send_command(command, response_timeout)
725
+
726
+ # Check the return parameters if required
727
+ if check_result:
728
+ if isinstance(response, hci.HCI_Command_Status_Event):
729
+ status = response.status # type: ignore[attr-defined]
730
+ elif isinstance(response.return_parameters, int):
731
+ status = response.return_parameters
732
+ elif isinstance(response.return_parameters, bytes):
733
+ # return parameters first field is a one byte status code
734
+ status = response.return_parameters[0]
735
+ elif isinstance(
736
+ response.return_parameters, hci.HCI_GenericReturnParameters
737
+ ):
738
+ # FIXME: temporary workaround
739
+ # NO STATUS
740
+ status = hci.HCI_SUCCESS
741
+ else:
742
+ status = response.return_parameters.status
743
+
744
+ if status != hci.HCI_SUCCESS:
745
+ logger.warning(
746
+ f'{command.name} failed ' f'({hci.HCI_Constant.error_name(status)})'
747
+ )
748
+ raise hci.HCI_Error(status)
749
+
750
+ return response
751
+
752
+ async def send_sync_command(
753
+ self, command: hci.HCI_SyncCommand[_RP], response_timeout: float | None = None
754
+ ) -> _RP:
755
+ response = await self.send_sync_command_raw(command, response_timeout)
756
+ return_parameters = response.return_parameters
757
+
758
+ # Check the return parameters's status
759
+ if isinstance(return_parameters, hci.HCI_StatusReturnParameters):
760
+ status = return_parameters.status
761
+ elif isinstance(return_parameters, hci.HCI_GenericReturnParameters):
762
+ # if the payload has at least one byte, assume the first byte is the status
763
+ if not return_parameters.data:
764
+ raise RuntimeError('no status byte in return parameters')
765
+ status = hci.HCI_ErrorCode(return_parameters.data[0])
766
+ else:
767
+ raise RuntimeError(
768
+ f'unexpected return parameters type ({type(return_parameters)})'
769
+ )
770
+ if status != hci.HCI_ErrorCode.SUCCESS:
771
+ logger.warning(
772
+ f'{command.name} failed ' f'({hci.HCI_Constant.error_name(status)})'
773
+ )
774
+ raise hci.HCI_Error(status)
775
+
776
+ return return_parameters
777
+
778
+ async def send_sync_command_raw(
779
+ self,
780
+ command: hci.HCI_SyncCommand[_RP],
781
+ response_timeout: float | None = None,
782
+ ) -> hci.HCI_Command_Complete_Event[_RP]:
783
+ response = await self._send_command(command, response_timeout)
784
+
785
+ # For unknown HCI commands, some controllers return Command Status instead of
786
+ # Command Complete.
787
+ if (
788
+ isinstance(response, hci.HCI_Command_Status_Event)
789
+ and response.status == hci.HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR
790
+ ):
791
+ return hci.HCI_Command_Complete_Event(
792
+ num_hci_command_packets=response.num_hci_command_packets,
793
+ command_opcode=command.op_code,
794
+ return_parameters=hci.HCI_StatusReturnParameters(
795
+ status=hci.HCI_ErrorCode(response.status)
796
+ ), # type: ignore
797
+ )
798
+
799
+ # Check that the response is of the expected type
800
+ assert isinstance(response, hci.HCI_Command_Complete_Event)
801
+
802
+ return response
803
+
804
+ async def send_async_command(
805
+ self,
806
+ command: hci.HCI_AsyncCommand,
807
+ check_status: bool = True,
808
+ response_timeout: float | None = None,
809
+ ) -> hci.HCI_ErrorCode:
810
+ response = await self._send_command(command, response_timeout)
811
+
812
+ # For unknown HCI commands, some controllers return Command Complete instead of
813
+ # Command Status.
814
+ if isinstance(response, hci.HCI_Command_Complete_Event):
815
+ # Assume the first byte of the return parameters is the status
816
+ if (
817
+ status := hci.HCI_ErrorCode(response.parameters[3])
818
+ ) != hci.HCI_ErrorCode.UNKNOWN_HCI_COMMAND_ERROR:
819
+ logger.warning(f'unexpected return paramerers status {status}')
820
+ else:
821
+ assert isinstance(response, hci.HCI_Command_Status_Event)
822
+ status = hci.HCI_ErrorCode(response.status)
823
+
824
+ # Check the status if required
825
+ if check_status:
826
+ if status != hci.HCI_CommandStatus.PENDING:
827
+ logger.warning(f'{command.name} failed ' f'({status.name})')
828
+ raise hci.HCI_Error(status)
829
+
830
+ return status
831
+
832
+ @utils.deprecated("Use utils.AsyncRunner.spawn() instead.")
833
+ def send_command_sync(self, command: hci.HCI_AsyncCommand) -> None:
834
+ utils.AsyncRunner.spawn(self.send_async_command(command))
707
835
 
708
836
  def send_acl_sdu(self, connection_handle: int, sdu: bytes) -> None:
709
837
  if not (connection := self.connections.get(connection_handle)):
@@ -728,7 +856,9 @@ class Host(utils.EventEmitter):
728
856
  data=pdu,
729
857
  )
730
858
  logger.debug(
731
- '>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
859
+ '>>> ACL packet enqueue: (handle=0x%04X) %s',
860
+ connection_handle,
861
+ pdu.hex(),
732
862
  )
733
863
  packet_queue.enqueue(acl_packet, connection_handle)
734
864
 
@@ -826,16 +956,18 @@ class Host(utils.EventEmitter):
826
956
  if self.local_supported_commands & mask
827
957
  )
828
958
 
829
- def supports_le_features(self, feature: hci.LeFeatureMask) -> bool:
830
- return (self.local_le_features & feature) == feature
959
+ def supports_le_features(self, features: hci.LeFeatureMask) -> bool:
960
+ return (self.local_le_features & features) == features
831
961
 
832
- def supports_lmp_features(self, feature: hci.LmpFeatureMask) -> bool:
833
- return self.local_lmp_features & (feature) == feature
962
+ def supports_lmp_features(self, features: hci.LmpFeatureMask) -> bool:
963
+ return self.local_lmp_features & (features) == features
834
964
 
835
965
  @property
836
- def supported_le_features(self):
966
+ def supported_le_features(self) -> list[hci.LeFeature]:
837
967
  return [
838
- feature for feature in range(64) if self.local_le_features & (1 << feature)
968
+ feature
969
+ for feature in hci.LeFeature
970
+ if self.local_le_features & (1 << feature)
839
971
  ]
840
972
 
841
973
  # Packet Sink protocol (packets coming from the controller via HCI)
@@ -924,6 +1056,8 @@ class Host(utils.EventEmitter):
924
1056
  self.pending_response.set_result(event)
925
1057
  else:
926
1058
  logger.warning('!!! no pending response future to set')
1059
+ if event.num_hci_command_packets and self.command_semaphore.locked():
1060
+ self.command_semaphore.release()
927
1061
 
928
1062
  ############################################################
929
1063
  # HCI handlers
@@ -935,7 +1069,13 @@ class Host(utils.EventEmitter):
935
1069
  if event.command_opcode == 0:
936
1070
  # This is used just for the Num_HCI_Command_Packets field, not related to
937
1071
  # an actual command
938
- logger.debug('no-command event')
1072
+ logger.debug('no-command event for flow control')
1073
+
1074
+ # Release the command semaphore if needed
1075
+ if event.num_hci_command_packets and self.command_semaphore.locked():
1076
+ logger.debug('command complete event releasing semaphore')
1077
+ self.command_semaphore.release()
1078
+
939
1079
  return
940
1080
 
941
1081
  return self.on_command_processed(event)
@@ -1116,7 +1256,7 @@ class Host(utils.EventEmitter):
1116
1256
  self, event: hci.HCI_LE_Connection_Update_Complete_Event
1117
1257
  ):
1118
1258
  if (connection := self.connections.get(event.connection_handle)) is None:
1119
- logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
1259
+ logger.warning('!!! CONNECTION UPDATE COMPLETE: unknown handle')
1120
1260
  return
1121
1261
 
1122
1262
  # Notify the client
@@ -1133,6 +1273,29 @@ class Host(utils.EventEmitter):
1133
1273
  'connection_parameters_update_failure', connection.handle, event.status
1134
1274
  )
1135
1275
 
1276
+ def on_hci_le_connection_rate_change_event(
1277
+ self, event: hci.HCI_LE_Connection_Rate_Change_Event
1278
+ ):
1279
+ if (connection := self.connections.get(event.connection_handle)) is None:
1280
+ logger.warning('!!! CONNECTION RATE CHANGE: unknown handle')
1281
+ return
1282
+
1283
+ # Notify the client
1284
+ if event.status == hci.HCI_SUCCESS:
1285
+ self.emit(
1286
+ 'le_connection_rate_change',
1287
+ connection.handle,
1288
+ event.connection_interval,
1289
+ event.subrate_factor,
1290
+ event.peripheral_latency,
1291
+ event.continuation_number,
1292
+ event.supervision_timeout,
1293
+ )
1294
+ else:
1295
+ self.emit(
1296
+ 'le_connection_rate_change_failure', connection.handle, event.status
1297
+ )
1298
+
1136
1299
  def on_hci_le_phy_update_complete_event(
1137
1300
  self, event: hci.HCI_LE_PHY_Update_Complete_Event
1138
1301
  ):
@@ -1348,15 +1511,17 @@ class Host(utils.EventEmitter):
1348
1511
 
1349
1512
  # For now, just accept everything
1350
1513
  # TODO: delegate the decision
1351
- self.send_command_sync(
1352
- hci.HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(
1353
- connection_handle=event.connection_handle,
1354
- interval_min=event.interval_min,
1355
- interval_max=event.interval_max,
1356
- max_latency=event.max_latency,
1357
- timeout=event.timeout,
1358
- min_ce_length=0,
1359
- max_ce_length=0,
1514
+ utils.AsyncRunner.spawn(
1515
+ self.send_sync_command(
1516
+ hci.HCI_LE_Remote_Connection_Parameter_Request_Reply_Command(
1517
+ connection_handle=event.connection_handle,
1518
+ interval_min=event.interval_min,
1519
+ interval_max=event.interval_max,
1520
+ max_latency=event.max_latency,
1521
+ timeout=event.timeout,
1522
+ min_ce_length=0,
1523
+ max_ce_length=0,
1524
+ )
1360
1525
  )
1361
1526
  )
1362
1527
 
@@ -1392,9 +1557,9 @@ class Host(utils.EventEmitter):
1392
1557
  connection_handle=event.connection_handle
1393
1558
  )
1394
1559
 
1395
- await self.send_command(response)
1560
+ await self.send_sync_command(response)
1396
1561
 
1397
- asyncio.create_task(send_long_term_key())
1562
+ utils.AsyncRunner.spawn(send_long_term_key())
1398
1563
 
1399
1564
  def on_hci_synchronous_connection_complete_event(
1400
1565
  self, event: hci.HCI_Synchronous_Connection_Complete_Event
@@ -1593,9 +1758,9 @@ class Host(utils.EventEmitter):
1593
1758
  bd_addr=event.bd_addr
1594
1759
  )
1595
1760
 
1596
- await self.send_command(response)
1761
+ await self.send_sync_command(response)
1597
1762
 
1598
- asyncio.create_task(send_link_key())
1763
+ utils.AsyncRunner.spawn(send_link_key())
1599
1764
 
1600
1765
  def on_hci_io_capability_request_event(
1601
1766
  self, event: hci.HCI_IO_Capability_Request_Event
@@ -1690,12 +1855,13 @@ class Host(utils.EventEmitter):
1690
1855
  self.emit(
1691
1856
  'le_remote_features_failure', event.connection_handle, event.status
1692
1857
  )
1693
- else:
1694
- self.emit(
1695
- 'le_remote_features',
1696
- event.connection_handle,
1697
- int.from_bytes(event.le_features, 'little'),
1698
- )
1858
+ return
1859
+
1860
+ self.emit(
1861
+ 'le_remote_features',
1862
+ event.connection_handle,
1863
+ hci.LeFeatureMask(int.from_bytes(event.le_features, 'little')),
1864
+ )
1699
1865
 
1700
1866
  def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
1701
1867
  self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
@@ -1728,6 +1894,12 @@ class Host(utils.EventEmitter):
1728
1894
  self.emit('cs_subevent_result_continue', event)
1729
1895
 
1730
1896
  def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
1897
+ if event.status != hci.HCI_SUCCESS:
1898
+ self.emit(
1899
+ 'le_subrate_change_failure', event.connection_handle, event.status
1900
+ )
1901
+ return
1902
+
1731
1903
  self.emit(
1732
1904
  'le_subrate_change',
1733
1905
  event.connection_handle,