From aa658418bcdbafa85246b5dde3d118d2ae552783 Mon Sep 17 00:00:00 2001 From: Ford Peprah Date: Wed, 14 Aug 2024 13:58:02 -0400 Subject: [PATCH 1/3] Bug: Edimax BLE Dongle Fails After Teardown and Re-Instantiation This patch addresses an issue where the some RTK BLE dongles fail to perform an HCI reset after the transport is torn down and re-instantiated. To address that, we prevent crashing the background threads when invalid data comes in, and time out if no response is received within a fixed amount of time. When the timeout occurs, we retry the reset, and ultimately skip over reading the local version information if that fails. --- bumble/drivers/rtk.py | 22 +++++++++++++++++----- bumble/host.py | 16 ++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/bumble/drivers/rtk.py b/bumble/drivers/rtk.py index 1336d2c..e7e476d 100644 --- a/bumble/drivers/rtk.py +++ b/bumble/drivers/rtk.py @@ -301,6 +301,8 @@ class Driver(common.Driver): fw_name: str = "" config_name: str = "" + POST_RESET_DELAY: float = 0.2 + DRIVER_INFOS = [ # 8723A DriverInfo( @@ -495,12 +497,22 @@ class Driver(common.Driver): @classmethod async def driver_info_for_host(cls, host): - await host.send_command(HCI_Reset_Command(), check_result=True) - host.ready = True # Needed to let the host know the controller is ready. + try: + await host.send_command( + HCI_Reset_Command(), check_result=True, response_timeout=cls.POST_RESET_DELAY + ) + host.ready = True # Needed to let the host know the controller is ready. + except asyncio.exceptions.TimeoutError: + logger.warning("timeout waiting for hci reset, retrying") + await host.send_command(HCI_Reset_Command(), check_result=True) + host.ready = True + + command = HCI_Read_Local_Version_Information_Command() + response = await host.send_command(command, check_result=True) + if response.command_opcode != command.op_code: + logger.error("failed to probe local version information") + return None - response = await host.send_command( - HCI_Read_Local_Version_Information_Command(), check_result=True - ) local_version = response.return_parameters logger.debug( diff --git a/bumble/host.py b/bumble/host.py index 8085d5c..1abefd9 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -514,7 +514,7 @@ class Host(AbortableEventEmitter): if self.hci_sink: self.hci_sink.on_packet(bytes(packet)) - async def send_command(self, command, check_result=False): + async def send_command(self, command, check_result=False, response_timeout: Optional[int] = None): # Wait until we can send (only one pending command at a time) async with self.command_semaphore: assert self.pending_command is None @@ -526,7 +526,8 @@ class Host(AbortableEventEmitter): try: self.send_hci_packet(command) - response = await self.pending_response + await asyncio.wait_for(self.pending_response, timeout=response_timeout) + response = self.pending_response.result() # Check the return parameters if required if check_result: @@ -625,14 +626,21 @@ class Host(AbortableEventEmitter): # Packet Sink protocol (packets coming from the controller via HCI) def on_packet(self, packet: bytes) -> None: - hci_packet = hci.HCI_Packet.from_bytes(packet) + try: + hci_packet = hci.HCI_Packet.from_bytes(packet) + except Exception as error: + logger.warning(f'!!! error parsing packet from bytes: {error}') + return + if self.ready or ( isinstance(hci_packet, hci.HCI_Command_Complete_Event) and hci_packet.command_opcode == hci.HCI_RESET_COMMAND ): self.on_hci_packet(hci_packet) else: - logger.debug('reset not done, ignoring packet from controller') + logger.debug( + f'reset not done, ignoring packet from controller: {hci_packet}' + ) def on_transport_lost(self): # Called by the source when the transport has been lost. From 88e3a2b87fda40f11d81faeb273e62ed75ee89af Mon Sep 17 00:00:00 2001 From: Ford Peprah Date: Thu, 29 Aug 2024 10:01:41 -0400 Subject: [PATCH 2/3] Fix linting errors. --- bumble/drivers/rtk.py | 4 +++- bumble/host.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bumble/drivers/rtk.py b/bumble/drivers/rtk.py index e7e476d..c332bf0 100644 --- a/bumble/drivers/rtk.py +++ b/bumble/drivers/rtk.py @@ -499,7 +499,9 @@ class Driver(common.Driver): async def driver_info_for_host(cls, host): try: await host.send_command( - HCI_Reset_Command(), check_result=True, response_timeout=cls.POST_RESET_DELAY + HCI_Reset_Command(), + check_result=True, + response_timeout=cls.POST_RESET_DELAY, ) host.ready = True # Needed to let the host know the controller is ready. except asyncio.exceptions.TimeoutError: diff --git a/bumble/host.py b/bumble/host.py index 1abefd9..6c2c350 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -514,7 +514,9 @@ class Host(AbortableEventEmitter): if self.hci_sink: self.hci_sink.on_packet(bytes(packet)) - async def send_command(self, command, check_result=False, response_timeout: Optional[int] = None): + async def send_command( + self, command, check_result=False, response_timeout: Optional[int] = None + ): # Wait until we can send (only one pending command at a time) async with self.command_semaphore: assert self.pending_command is None From b7259abe3c26e8ad6aaa658f9a813c13e670b3f7 Mon Sep 17 00:00:00 2001 From: Ford Peprah Date: Tue, 10 Sep 2024 10:59:46 -0400 Subject: [PATCH 3/3] Fix typing errors. --- bumble/host.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bumble/host.py b/bumble/host.py index 6c2c350..a3d3dad 100644 --- a/bumble/host.py +++ b/bumble/host.py @@ -171,7 +171,7 @@ class Host(AbortableEventEmitter): self.cis_links = {} # CIS links, by connection handle self.sco_links = {} # SCO links, by connection handle self.pending_command = None - self.pending_response = None + self.pending_response: Optional[asyncio.Future[Any]] = None self.number_of_supported_advertising_sets = 0 self.maximum_advertising_data_length = 31 self.local_version = None @@ -534,7 +534,7 @@ class Host(AbortableEventEmitter): # Check the return parameters if required if check_result: if isinstance(response, hci.HCI_Command_Status_Event): - status = response.status + status = response.status # type: ignore[attr-defined] elif isinstance(response.return_parameters, int): status = response.return_parameters elif isinstance(response.return_parameters, bytes):