When Host._send_command times out waiting for an HCI response, the command_semaphore is never released. Every subsequent call to _send_command then blocks indefinitely at await self.command_semaphore.acquire(), making the host permanently unresponsive with no error or exception.
The release in the finally block is gated on response is not None. On a timeout, response remains None, so the semaphore is never released:
response = None
try:
...
response = await asyncio.wait_for(self.pending_response, timeout=response_timeout)
return response
finally:
if (
response is not None # <-- None on timeout
and response.num_hci_command_packets
and self.command_semaphore.locked()
):
self.command_semaphore.release() # <-- never reached
I'm wondering if this is intentional, to perhaps indicate that the controller is unresponsive/pending? The controller I'm using definitely isn't the most reliable (Raspberry Pi 3B+), but I'm not sure how to detect / recover from this, other than wrapping all bumble calls in a timeout.
There was about 3 minutes between when I issued the command that timed out (!!! Command HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND timed out) to when I tried issuing the next command.
I haven't been able to capture any proper logs of this happening on real hardware, but this script reproduces what I've seen (reproduction code generated with Claude).
import asyncio
from bumble import hci
from bumble.controller import Controller
from bumble.device import Device
from bumble.host import Host
from bumble.link import LocalLink
from bumble.transport.common import AsyncPipeSink
def make_device() -> Device:
link = LocalLink()
controller = Controller("test-controller", link=link)
device = Device(host=Host())
device.host.set_packet_sink(controller)
controller.host = AsyncPipeSink(device.host)
return device
async def main():
device = make_device()
await device.power_on()
print("Step 1: Send a command with a near-zero timeout to force a timeout...")
try:
await device.host._send_command(
hci.HCI_LE_Clear_Filter_Accept_List_Command(),
response_timeout=0.00001,
)
except TimeoutError:
print(" -> Timed out as expected.")
print("Step 2: Send a normal command — hangs forever if semaphore was leaked...")
try:
await asyncio.wait_for(
device.host._send_command(
hci.HCI_LE_Clear_Filter_Accept_List_Command(),
),
timeout=3.0,
)
print(" -> Completed (bug is fixed).")
except asyncio.TimeoutError:
print(
" -> HUNG for 3 seconds and timed out: semaphore was leaked. Bug confirmed."
)
asyncio.run(main())
When Host._send_command times out waiting for an HCI response, the command_semaphore is never released. Every subsequent call to _send_command then blocks indefinitely at await self.command_semaphore.acquire(), making the host permanently unresponsive with no error or exception.
The release in the finally block is gated on response is not None. On a timeout, response remains None, so the semaphore is never released:
I'm wondering if this is intentional, to perhaps indicate that the controller is unresponsive/pending? The controller I'm using definitely isn't the most reliable (Raspberry Pi 3B+), but I'm not sure how to detect / recover from this, other than wrapping all bumble calls in a timeout.
There was about 3 minutes between when I issued the command that timed out (
!!! Command HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND timed out) to when I tried issuing the next command.I haven't been able to capture any proper logs of this happening on real hardware, but this script reproduces what I've seen (reproduction code generated with Claude).