From d04e4c8370b40a9919f26da28304482487fd957f Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 1 Jul 2026 15:31:52 +0100 Subject: [PATCH] fix: clean MeshCore adapter shutdown close() stopped the asyncio loop without disconnecting first, leaving the EventDispatcher._process_events() task and the serial transport pending ("Task was destroyed but it is pending!" / "Event loop is closed"). Now await MeshCore.disconnect() and let the transport's connection_lost callback settle before stopping the loop, then join the thread. Found by running the adapter against a live MeshCore-flashed Heltec V3. Co-Authored-By: Claude Opus 4.8 (1M context) --- host-bridge/adapters/meshcore_adapter.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/host-bridge/adapters/meshcore_adapter.py b/host-bridge/adapters/meshcore_adapter.py index cc98ebd..b474577 100644 --- a/host-bridge/adapters/meshcore_adapter.py +++ b/host-bridge/adapters/meshcore_adapter.py @@ -79,5 +79,25 @@ class MeshCoreAdapter: ) def close(self) -> None: - if self._loop is not None: - self._loop.call_soon_threadsafe(self._loop.stop) + if self._loop is None: + return + # Disconnect on the loop thread first: MeshCore.disconnect() stops the + # EventDispatcher task and closes the serial transport. Skipping this + # and stopping the loop directly leaves _process_events() pending + # ("Task was destroyed but it is pending!" / "Event loop is closed"). + if self._mc is not None: + async def _shutdown() -> None: + await self._mc.disconnect() + # Let the serial transport's connection_lost callback run to + # completion before the loop stops, otherwise it lingers as a + # pending SerialTransport._call_connection_lost task. + await asyncio.sleep(0.25) + + try: + fut = asyncio.run_coroutine_threadsafe(_shutdown(), self._loop) + fut.result(timeout=5) + except Exception: + pass + self._loop.call_soon_threadsafe(self._loop.stop) + if self._thread is not None: + self._thread.join(timeout=5)