From 18f76d5da7bd0dd4d2a0f32dfb59804b022ca64b Mon Sep 17 00:00:00 2001 From: Szymon Paluch Date: Fri, 6 Mar 2026 23:07:48 +0100 Subject: [PATCH 1/2] fix: add 30s timeout to WebRTC connection_ready.wait() to prevent indefinite hang Blueprint.build() and direct UnitreeWebRTCConnection() hang forever when the WebRTC SDP exchange fails (port closed, another client connected, robot unreachable). connection_ready.wait() has no timeout, blocking the subprocess/main thread indefinitely with no error message. Add a 30-second timeout with thread cleanup and a descriptive TimeoutError listing common causes and troubleshooting steps. Common trigger: Go2 only allows one WebRTC client at a time. If the Unitree mobile app is connected, the SDP exchange returns 'reject' and the event never fires. Error now surfaces immediately instead of hanging forever. Tested: Go2 Pro, stock firmware, STA mode (home WiFi), DimOS v0.0.10.post2 --- dimos/robot/unitree/connection.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/dimos/robot/unitree/connection.py b/dimos/robot/unitree/connection.py index f3f8ffaafb..29999941ba 100644 --- a/dimos/robot/unitree/connection.py +++ b/dimos/robot/unitree/connection.py @@ -111,10 +111,25 @@ def start_background_loop() -> None: self.task = self.loop.create_task(async_connect()) self.loop.run_forever() + CONNECT_TIMEOUT = 30 # seconds + self.loop = asyncio.new_event_loop() self.thread = threading.Thread(target=start_background_loop, daemon=True) self.thread.start() - self.connection_ready.wait() + + if not self.connection_ready.wait(timeout=CONNECT_TIMEOUT): + if self.loop.is_running(): + self.loop.call_soon_threadsafe(self.loop.stop) + if self.thread.is_alive(): + self.thread.join(timeout=2.0) + raise TimeoutError( + f"WebRTC connection to {self.ip} timed out after {CONNECT_TIMEOUT}s. " + "Common causes:\n" + " - Another WebRTC client is connected (close the Unitree mobile app)\n" + " - Robot is unreachable on the network\n" + " - Port 9991 (encrypted SDP) is not responding\n" + "Tip: only one WebRTC client can connect to the Go2 at a time." + ) def start(self) -> None: pass From 2aefe0cc8cce008a179a8bc09646cbe2e0c1145e Mon Sep 17 00:00:00 2001 From: Szymon Paluch Date: Fri, 6 Mar 2026 23:51:50 +0100 Subject: [PATCH 2/2] fix: cancel task and disconnect on timeout to prevent retry hang Address review feedback: the timeout cleanup path was missing task.cancel() and conn.disconnect(), which could leave a half-open WebRTC session on the robot side. A subsequent connect() call would then time out again for the same reason, recreating the original infinite-hang problem. Cleanup sequence on timeout: 1. Cancel the async_connect task (CancelledError handled) 2. Call conn.disconnect() if conn was partially initialised 3. Stop the event loop 4. Join the background thread (2s timeout) 5. Raise TimeoutError with diagnostic message --- dimos/robot/unitree/connection.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dimos/robot/unitree/connection.py b/dimos/robot/unitree/connection.py index 29999941ba..1c8aa75087 100644 --- a/dimos/robot/unitree/connection.py +++ b/dimos/robot/unitree/connection.py @@ -118,7 +118,28 @@ def start_background_loop() -> None: self.thread.start() if not self.connection_ready.wait(timeout=CONNECT_TIMEOUT): + # Cancel the async connection task and disconnect to avoid leaving + # a half-open WebRTC session on the robot side (which would cause + # the next connection attempt to hang as well). + async def _cleanup() -> None: + if self.task is not None: + self.task.cancel() + try: + await self.task + except (asyncio.CancelledError, Exception): + pass + if hasattr(self, "conn") and self.conn is not None: + try: + await self.conn.disconnect() + except Exception: + pass + if self.loop.is_running(): + future = asyncio.run_coroutine_threadsafe(_cleanup(), self.loop) + try: + future.result(timeout=5.0) + except Exception: + pass self.loop.call_soon_threadsafe(self.loop.stop) if self.thread.is_alive(): self.thread.join(timeout=2.0)