Skip to content

Commit 1ad5b57

Browse files
authored
Move retrying out of connect and into startup_reset (#696)
* Move retrying out of `connect` and into `startup_reset` * Constant tweaking * Better debug logging for long CANCEL sequences * Fix tests
1 parent 54a8e32 commit 1ad5b57

File tree

6 files changed

+54
-22
lines changed

6 files changed

+54
-22
lines changed

bellows/ash.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from zigpy.types import BaseDataclassMixin
1717

1818
import bellows.types as t
19+
from bellows.zigbee.util import run_length_debug
1920

2021
_LOGGER = logging.getLogger(__name__)
2122

@@ -596,8 +597,13 @@ def _write_frame(
596597
raise NcpFailure("Transport is closed, cannot send frame")
597598

598599
if _LOGGER.isEnabledFor(logging.DEBUG):
599-
prefix_str = "".join([f"{r.name} + " for r in prefix])
600-
suffix_str = "".join([f" + {r.name}" for r in suffix])
600+
prefix_str = run_length_debug(
601+
[p.name for p in prefix], joiner=" + ", suffix=" "
602+
)
603+
suffix_str = run_length_debug(
604+
[s.name for s in suffix], joiner=" + ", prefix=" "
605+
)
606+
601607
_LOGGER.debug("Sending frame %s%r%s", prefix_str, frame, suffix_str)
602608

603609
data = bytes(prefix) + self._stuff_bytes(frame.to_bytes()) + bytes(suffix)
@@ -713,7 +719,4 @@ async def send_data(self, data: bytes) -> None:
713719

714720
def send_reset(self) -> None:
715721
# Some adapters seem to send a NAK immediately but still process the reset frame
716-
# if one eventually makes it through
717-
self._write_frame(RstFrame(), prefix=(Reserved.CANCEL,))
718-
self._write_frame(RstFrame(), prefix=(Reserved.CANCEL,))
719-
self._write_frame(RstFrame(), prefix=(Reserved.CANCEL,))
722+
self._write_frame(RstFrame(), prefix=40 * (Reserved.CANCEL,))

bellows/ezsp/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from . import v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v16, v17
2828

29-
RESET_ATTEMPTS = 5
29+
RESET_ATTEMPTS = 3
3030

3131
EZSP_LATEST = v17.EZSPv17.VERSION
3232
LOGGER = logging.getLogger(__name__)
@@ -109,7 +109,7 @@ def is_tcp_serial_port(self) -> bool:
109109
parsed_path = urllib.parse.urlparse(self._config[conf.CONF_DEVICE_PATH])
110110
return parsed_path.scheme == "socket"
111111

112-
async def startup_reset(self) -> None:
112+
async def _startup_reset(self) -> None:
113113
"""Start EZSP and reset the stack."""
114114
# `zigbeed` resets on startup
115115
if self.is_tcp_serial_port:
@@ -128,15 +128,12 @@ async def startup_reset(self) -> None:
128128
await self.version()
129129
await self.get_xncp_features()
130130

131-
async def connect(self, *, use_thread: bool = True) -> None:
132-
assert self._gw is None
133-
self._gw = await bellows.uart.connect(self._config, self, use_thread=use_thread)
134-
131+
async def startup_reset(self) -> None:
135132
for attempt in range(RESET_ATTEMPTS):
136133
self._protocol = v4.EZSPv4(self.handle_callback, self._gw)
137134

138135
try:
139-
await self.startup_reset()
136+
await self._startup_reset()
140137
break
141138
except Exception as exc:
142139
if attempt + 1 < RESET_ATTEMPTS:
@@ -151,6 +148,11 @@ async def connect(self, *, use_thread: bool = True) -> None:
151148
await self.disconnect()
152149
raise
153150

151+
async def connect(self, *, use_thread: bool = True) -> None:
152+
assert self._gw is None
153+
self._gw = await bellows.uart.connect(self._config, self, use_thread=use_thread)
154+
await self.startup_reset()
155+
154156
async def reset(self):
155157
LOGGER.debug("Resetting EZSP")
156158
self.stop_ezsp()

bellows/uart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import bellows.types as t
1111

1212
LOGGER = logging.getLogger(__name__)
13-
RESET_TIMEOUT = 3
13+
RESET_TIMEOUT = 2.5
1414

1515

1616
class Gateway(zigpy.serial.SerialProtocol):

bellows/zigbee/util.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Sequence
12
import logging
23
import math
34

@@ -129,3 +130,32 @@ def map_energy_to_rssi(lqi: float) -> float:
129130
x_0=RSSI_MIN + 0.45 * (RSSI_MAX - RSSI_MIN),
130131
k=0.13,
131132
)
133+
134+
135+
def run_length_debug(
136+
items: Sequence[str],
137+
joiner: str,
138+
prefix: str = "",
139+
suffix: str = "",
140+
default: str = "",
141+
) -> str:
142+
"""Create a run-length encoded debug string from a sequence of strings."""
143+
counts = []
144+
unique_items = []
145+
146+
for item in items:
147+
if unique_items and unique_items[-1] == item:
148+
counts[-1] += 1
149+
else:
150+
counts.append(1)
151+
unique_items.append(item)
152+
153+
result = joiner.join(
154+
f"{count}*{item}" if count > 1 else item
155+
for item, count in zip(unique_items, counts)
156+
)
157+
158+
if not result:
159+
return default
160+
161+
return prefix + result + suffix

tests/test_ash.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,10 +438,7 @@ async def test_ash_protocol_startup(caplog):
438438

439439
assert ezsp.reset_received.mock_calls == [call(t.NcpResetCode.RESET_SOFTWARE)]
440440
assert protocol._write_frame.mock_calls == [
441-
# We send three
442-
call(ash.RstFrame(), prefix=(ash.Reserved.CANCEL,)),
443-
call(ash.RstFrame(), prefix=(ash.Reserved.CANCEL,)),
444-
call(ash.RstFrame(), prefix=(ash.Reserved.CANCEL,)),
441+
call(ash.RstFrame(), prefix=40 * (ash.Reserved.CANCEL,))
445442
]
446443

447444
protocol._write_frame.reset_mock()

tests/test_ezsp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,12 @@ async def test_ezsp_connect_failure(disconnect_mock, reset_mock, version_mock):
299299
await ezsp.connect()
300300

301301
assert conn_mock.await_count == 1
302-
assert reset_mock.await_count == 5
303-
assert version_mock.await_count == 5
302+
assert reset_mock.await_count == 3
303+
assert version_mock.await_count == 3
304304
assert disconnect_mock.call_count == 1
305305

306306

307-
@pytest.mark.parametrize("failures_before_success", [1, 2, 3, 4])
307+
@pytest.mark.parametrize("failures_before_success", [1, 2])
308308
@patch.object(EZSP, "disconnect", new_callable=AsyncMock)
309309
async def test_ezsp_connect_retry_success(disconnect_mock, failures_before_success):
310310
"""Test connection succeeding after N failures."""
@@ -319,7 +319,7 @@ async def startup_reset_mock():
319319
with patch("bellows.uart.connect"):
320320
ezsp = make_ezsp(version=4)
321321

322-
with patch.object(ezsp, "startup_reset", side_effect=startup_reset_mock):
322+
with patch.object(ezsp, "_startup_reset", side_effect=startup_reset_mock):
323323
await ezsp.connect()
324324

325325
assert call_count == failures_before_success + 1

0 commit comments

Comments
 (0)