Skip to content

Commit 2219b37

Browse files
committed
Reduce timeout to 1 second and other fixes.
1 parent d2699e2 commit 2219b37

File tree

2 files changed

+44
-13
lines changed

2 files changed

+44
-13
lines changed

pybricksdev/connections/pybricks.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,11 +839,17 @@ async def write_gatt_char(self, uuid: str, data, response: bool) -> None:
839839
self._ep_out.write(bytes([PybricksUsbOutEpMessageType.COMMAND]) + data)
840840

841841
try:
842+
# FIXME: race_disconnect() doesn't work properly for USB connections since
843+
# pyusb doesn't provide a reliable way to detect disconnects. The disconnect
844+
# event from the USB stack may not be received in time to cancel the wait
845+
# operation. We should implement a proper USB disconnect detection mechanism.
842846
reply = await asyncio.wait_for(
843847
self.race_disconnect(self._response_queue.get()),
844-
timeout=5.0, # 5-second timeout
848+
timeout=1.0,
845849
)
846850
except asyncio.TimeoutError:
851+
if self.connection_state_observable.value != ConnectionState.CONNECTED:
852+
raise RuntimeError("Hub disconnected while waiting for response")
847853
raise asyncio.TimeoutError("Timeout waiting for USB response")
848854

849855
# REVISIT: could look up status error code and convert to string,

tests/connections/test_pybricks.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
88

99
import pytest
10-
from reactivex.subject import Subject
10+
from reactivex.subject import BehaviorSubject, Subject
1111

1212
from pybricksdev.ble.pybricks import PYBRICKS_COMMAND_EVENT_UUID
1313
from pybricksdev.connections.pybricks import (
@@ -243,26 +243,51 @@ async def test_pybricks_hub_usb_write_gatt_char_timeout(self):
243243
# Make _response_queue.get() block indefinitely
244244
hub._response_queue.get = AsyncMock(side_effect=asyncio.Event().wait)
245245

246-
mock_observable = MagicMock(spec=Subject)
246+
mock_observable = MagicMock(spec=BehaviorSubject)
247+
mock_observable.value = ConnectionState.CONNECTED
248+
hub.connection_state_observable = mock_observable
247249

248-
def mock_subscribe_side_effect(on_next_callback, *args, **kwargs):
249-
mock_subscription = MagicMock()
250-
mock_subscription.dispose = MagicMock()
251-
return mock_subscription
250+
# Simulate a timeout while the hub is still connected
251+
with patch(
252+
"asyncio.wait_for", side_effect=asyncio.TimeoutError("Test-induced timeout")
253+
):
254+
with pytest.raises(
255+
asyncio.TimeoutError, match="Timeout waiting for USB response"
256+
):
257+
await hub.write_gatt_char(
258+
PYBRICKS_COMMAND_EVENT_UUID, b"test_data", True
259+
)
252260

253-
mock_observable.subscribe = MagicMock(side_effect=mock_subscribe_side_effect)
254-
type(hub.connection_state_observable).value = PropertyMock(
255-
return_value=ConnectionState.CONNECTED
261+
hub._ep_out.write.assert_called_once_with(
262+
bytes([PybricksUsbOutEpMessageType.COMMAND]) + b"test_data"
256263
)
264+
265+
@pytest.mark.asyncio
266+
async def test_pybricks_hub_usb_write_gatt_char_timeout_disconnected(self):
267+
"""Test write_gatt_char when a timeout occurs and hub is already disconnected.
268+
269+
This test documents the FIXME case where race_disconnect() doesn't work properly
270+
for USB connections because pyusb doesn't provide reliable disconnect detection.
271+
In this case, we might get a timeout while the hub is already disconnected,
272+
but the disconnect event wasn't received in time to cancel the wait operation.
273+
"""
274+
hub = PybricksHubUSB(MagicMock())
275+
276+
hub._ep_out = MagicMock()
277+
hub._response_queue = AsyncMock()
278+
# Make _response_queue.get() block indefinitely
279+
hub._response_queue.get = AsyncMock(side_effect=asyncio.Event().wait)
280+
281+
mock_observable = MagicMock(spec=BehaviorSubject)
282+
mock_observable.value = ConnectionState.DISCONNECTED
257283
hub.connection_state_observable = mock_observable
258284

259-
# The method has a hardcoded timeout of 5.0s.
260-
# We can patch asyncio.wait_for to speed up the test.
285+
# Simulate a timeout while the hub is already disconnected
261286
with patch(
262287
"asyncio.wait_for", side_effect=asyncio.TimeoutError("Test-induced timeout")
263288
):
264289
with pytest.raises(
265-
asyncio.TimeoutError, match="Timeout waiting for USB response"
290+
RuntimeError, match="Hub disconnected while waiting for response"
266291
):
267292
await hub.write_gatt_char(
268293
PYBRICKS_COMMAND_EVENT_UUID, b"test_data", True

0 commit comments

Comments
 (0)