From 9f7143452f40cfdf5edfe27fb77b74d5917611d7 Mon Sep 17 00:00:00 2001 From: aguilaair Date: Tue, 10 Nov 2026 07:56:12 -0800 Subject: [PATCH] Reset the protocol version before each startup attempt startup_reset() replaced the protocol object with v4 on every attempt but left _ezsp_version at whatever a failed attempt had negotiated. On the retry, version() then saw the stick's version equal to the stale one and never switched back from v4 framing, so later replies were misparsed and startup timed out. This happens with zigbeed over TCP when its reset takes longer than the startup wait (seen at ~4 s): ZHA's form/restore flows hang. Use _switch_protocol_version(v4) so the version and protocol object agree. --- bellows/ezsp/__init__.py | 5 ++++- tests/test_ezsp.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index a2113829..1aa308bd 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -144,7 +144,10 @@ async def _startup_reset(self) -> None: async def startup_reset(self) -> None: for attempt in range(RESET_ATTEMPTS): - self._protocol = v4.EZSPv4(self.handle_callback, self._gw) + # Start every attempt from a consistent v4 state. A failed attempt may + # already have switched to the stick's protocol version; keeping that + # version with a v4 protocol object makes `version()` skip the switch. + self._switch_protocol_version(v4.EZSPv4.VERSION) try: await self._startup_reset() diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index bccf17b3..8a8ba278 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -18,6 +18,7 @@ InvalidTxPower, ) from bellows.ezsp import EZSP, EZSP_LATEST, xncp +from bellows.ezsp.v4 import EZSPv4 from bellows.ezsp.v9.commands import GetTokenDataRsp import bellows.types as t @@ -341,6 +342,32 @@ async def startup_reset_mock(): assert disconnect_mock.call_count == 0 +@patch.object(EZSP, "disconnect", new_callable=AsyncMock) +async def test_ezsp_connect_retry_resets_protocol_version(disconnect_mock): + """A retry after a failure that already switched protocols starts from v4.""" + call_count = 0 + seen = [] + + async def startup_reset_mock(): + nonlocal call_count + call_count += 1 + seen.append((ezsp.ezsp_version, type(ezsp._protocol))) + if call_count == 1: + # The first attempt negotiates v12, then fails (e.g. a slow `zigbeed` reset) + ezsp._switch_protocol_version(12) + raise RuntimeError("Startup failed after the version switch") + + with patch("bellows.uart.connect"): + ezsp = make_ezsp(version=4) + + with patch.object(ezsp, "_startup_reset", side_effect=startup_reset_mock): + await ezsp.connect() + + assert call_count == 2 + assert seen[1] == (4, EZSPv4) + assert disconnect_mock.call_count == 0 + + async def test_ezsp_newer_version(ezsp_f): """Test newer version of ezsp.""" with patch.object(