Skip to content

Commit cea6de5

Browse files
committed
Cleanly handle connection loss
1 parent 3657cf3 commit cea6de5

File tree

4 files changed

+34
-80
lines changed

4 files changed

+34
-80
lines changed

bellows/ash.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def connection_made(self, transport):
367367
self._transport = transport
368368
self._ezsp_protocol.connection_made(self)
369369

370-
def connection_lost(self, exc):
370+
def connection_lost(self, exc: Exception | None) -> None:
371371
self._ezsp_protocol.connection_lost(exc)
372372

373373
def eof_received(self):

bellows/ezsp/__init__.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@ class EZSP:
5858
v13.EZSPv13.VERSION: v13.EZSPv13,
5959
}
6060

61-
def __init__(self, device_config: dict):
61+
def __init__(self, device_config: dict, application: Any | None = None):
6262
self._config = device_config
6363
self._callbacks = {}
6464
self._ezsp_event = asyncio.Event()
6565
self._ezsp_version = v4.EZSPv4.VERSION
6666
self._gw = None
6767
self._protocol = None
68+
self._application = application
6869
self._send_sem = PriorityDynamicBoundedSemaphore(value=MAX_COMMAND_CONCURRENCY)
6970

7071
self._stack_status_listeners: collections.defaultdict[
@@ -126,24 +127,18 @@ async def startup_reset(self) -> None:
126127

127128
await self.version()
128129

129-
@classmethod
130-
async def initialize(cls, zigpy_config: dict) -> EZSP:
131-
"""Return initialized EZSP instance."""
132-
ezsp = cls(zigpy_config[conf.CONF_DEVICE])
133-
await ezsp.connect(use_thread=zigpy_config[conf.CONF_USE_THREAD])
134-
135-
try:
136-
await ezsp.startup_reset()
137-
except Exception:
138-
ezsp.close()
139-
raise
140-
141-
return ezsp
142-
143130
async def connect(self, *, use_thread: bool = True) -> None:
144131
assert self._gw is None
145132
self._gw = await bellows.uart.connect(self._config, self, use_thread=use_thread)
146133
self._protocol = v4.EZSPv4(self.handle_callback, self._gw)
134+
await self.startup_reset()
135+
136+
async def disconnect(self) -> None:
137+
if self._gw is not None:
138+
await self._gw.disconnect()
139+
self._gw = None
140+
elif self._application is not None:
141+
self._application.connection_lost(None)
147142

148143
async def reset(self):
149144
LOGGER.debug("Resetting EZSP")
@@ -282,18 +277,13 @@ def connection_lost(self, exc):
282277
self._config[conf.CONF_DEVICE_PATH],
283278
exc,
284279
)
285-
self.enter_failed_state(f"Serial connection loss: {exc!r}")
280+
if self._application is not None:
281+
self._application.connection_lost(exc)
286282

287283
def enter_failed_state(self, error):
288284
"""UART received error frame."""
289-
if len(self._callbacks) > 1:
290-
LOGGER.error("NCP entered failed state. Requesting APP controller restart")
291-
self.close()
292-
self.handle_callback("_reset_controller_application", (error,))
293-
else:
294-
LOGGER.info(
295-
"NCP entered failed state. No application handler registered, ignoring..."
296-
)
285+
if self._application is not None:
286+
self._application.connection_lost(error)
297287

298288
def __getattr__(self, name: str) -> Callable:
299289
if name not in self._protocol.COMMANDS:

bellows/uart.py

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,15 @@
1818
RESET_TIMEOUT = 5
1919

2020

21-
class Gateway(asyncio.Protocol):
22-
FLAG = b"\x7E" # Marks end of frame
23-
ESCAPE = b"\x7D"
24-
XON = b"\x11" # Resume transmission
25-
XOFF = b"\x13" # Stop transmission
26-
SUBSTITUTE = b"\x18"
27-
CANCEL = b"\x1A" # Terminates a frame in progress
28-
STUFF = 0x20
29-
RANDOMIZE_START = 0x42
30-
RANDOMIZE_SEQ = 0xB8
31-
32-
RESERVED = FLAG + ESCAPE + XON + XOFF + SUBSTITUTE + CANCEL
33-
34-
class Terminator:
35-
pass
36-
37-
def __init__(self, application, connected_future=None, connection_done_future=None):
21+
class Gateway(zigpy.serial.SerialProtocol):
22+
def __init__(self, application, connection_done_future=None):
23+
super().__init__()
3824
self._application = application
3925

4026
self._reset_future = None
4127
self._startup_reset_future = None
42-
self._connected_future = connected_future
4328
self._connection_done_future = connection_done_future
4429

45-
self._transport = None
46-
47-
def close(self):
48-
self._transport.close()
49-
50-
def connection_made(self, transport):
51-
"""Callback when the uart is connected"""
52-
self._transport = transport
53-
if self._connected_future is not None:
54-
self._connected_future.set_result(True)
55-
5630
async def send_data(self, data: bytes) -> None:
5731
await self._transport.send_data(data)
5832

@@ -92,12 +66,9 @@ def _reset_cleanup(self, future):
9266
"""Delete reset future."""
9367
self._reset_future = None
9468

95-
def eof_received(self):
96-
"""Server gracefully closed its side of the connection."""
97-
self.connection_lost(ConnectionResetError("Remote server closed connection"))
98-
9969
def connection_lost(self, exc):
10070
"""Port was closed unexpectedly."""
71+
super().connection_lost(exc)
10172

10273
LOGGER.debug("Connection lost: %r", exc)
10374
reason = exc or ConnectionResetError("Remote server closed connection")
@@ -117,11 +88,6 @@ def connection_lost(self, exc):
11788
self._reset_future.set_exception(reason)
11889
self._reset_future = None
11990

120-
if exc is None:
121-
LOGGER.debug("Closed serial connection")
122-
return
123-
124-
LOGGER.error("Lost serial connection: %r", exc)
12591
self._application.connection_lost(exc)
12692

12793
async def reset(self):
@@ -144,10 +110,9 @@ async def reset(self):
144110
async def _connect(config, application):
145111
loop = asyncio.get_event_loop()
146112

147-
connection_future = loop.create_future()
148113
connection_done_future = loop.create_future()
149114

150-
gateway = Gateway(application, connection_future, connection_done_future)
115+
gateway = Gateway(application, connection_done_future)
151116
protocol = AshProtocol(gateway)
152117

153118
if config[zigpy.config.CONF_DEVICE_FLOW_CONTROL] is None:
@@ -164,7 +129,7 @@ async def _connect(config, application):
164129
rtscts=rtscts,
165130
)
166131

167-
await connection_future
132+
await gateway.wait_until_connected()
168133

169134
thread_safe_protocol = ThreadsafeProxy(gateway, loop)
170135
return thread_safe_protocol, connection_done_future

bellows/zigbee/application.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,22 +142,22 @@ async def _get_board_info(self) -> tuple[str, str, str] | tuple[None, None, None
142142
return None, None, None
143143

144144
async def connect(self) -> None:
145-
ezsp = bellows.ezsp.EZSP(self.config[zigpy.config.CONF_DEVICE])
146-
await ezsp.connect(use_thread=self.config[CONF_USE_THREAD])
145+
self._ezsp = bellows.ezsp.EZSP(self.config[zigpy.config.CONF_DEVICE], self)
147146

148147
try:
149-
await ezsp.startup_reset()
148+
await self._ezsp.connect(use_thread=self.config[CONF_USE_THREAD])
149+
await self._ezsp.startup_reset()
150150

151151
# Writing config is required here because network info can't be loaded
152-
await ezsp.write_config(self.config[CONF_EZSP_CONFIG])
153-
except Exception:
154-
ezsp.close()
155-
raise
156-
157-
self._ezsp = ezsp
152+
await self._ezsp.write_config(self.config[CONF_EZSP_CONFIG])
158153

159-
self._created_device_endpoints.clear()
160-
await self.register_endpoints()
154+
self._created_device_endpoints.clear()
155+
await self.register_endpoints()
156+
except Exception as exc:
157+
await self._ezsp.disconnect()
158+
self._ezsp = None
159+
self.connection_lost(exc)
160+
raise
161161

162162
async def _ensure_network_running(self) -> bool:
163163
"""Ensures the network is currently running and returns whether or not the network
@@ -603,8 +603,9 @@ async def disconnect(self):
603603
# TODO: how do you shut down the stack?
604604
self.controller_event.clear()
605605
if self._ezsp is not None:
606-
self._ezsp.close()
606+
await self._ezsp.disconnect()
607607
self._ezsp = None
608+
self.connection_lost(None)
608609

609610
async def force_remove(self, dev):
610611
# This should probably be delivered to the parent device instead
@@ -623,8 +624,6 @@ def ezsp_callback_handler(self, frame_name, args):
623624
self.handle_route_record(*args)
624625
elif frame_name == "incomingRouteErrorHandler":
625626
self.handle_route_error(*args)
626-
elif frame_name == "_reset_controller_application":
627-
self.connection_lost(args[0])
628627
elif frame_name == "idConflictHandler":
629628
self._handle_id_conflict(*args)
630629

0 commit comments

Comments
 (0)