Skip to content

Commit d5a28e0

Browse files
authored
Wait for serial port close when disconnecting (#84)
* Wait for the serial port to close * Remove unnecessary sleeps * Explicit `pyserial-asyncio-fast` dependency * Drop in-tree `SerialProtocol` * Drop hard bellows dependency * Add minimum zigpy dependency for `SerialProtocol` * `protocol.wait_closed()` -> `protocol.disconnect()` * Move `send_data` into the individual protocols * Mandate bellows>=0.0.42
1 parent 0eca699 commit d5a28e0

File tree

8 files changed

+47
-110
lines changed

8 files changed

+47
-110
lines changed

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
1717
"click>=8.0.0",
18-
"zigpy",
18+
"zigpy>=0.70.0",
1919
"crc",
20-
"bellows~=0.41.0",
20+
"bellows>=0.42.0",
2121
'gpiod; platform_system=="Linux"',
2222
"coloredlogs",
2323
"async_timeout",
2424
"typing_extensions",
25+
"pyserial-asyncio-fast",
2526
]
2627

2728
[tool.setuptools.packages.find]

universal_silabs_flasher/common.py

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import async_timeout
1313
import click
1414
import crc
15-
import serial_asyncio
1615
import zigpy.serial
1716

1817
if typing.TYPE_CHECKING:
@@ -118,61 +117,6 @@ async def wait_for_state(self, state: str) -> None:
118117
self._futures_for_state[state].remove(future)
119118

120119

121-
class SerialProtocol(asyncio.Protocol):
122-
"""Base class for packet-parsing serial protocol implementations."""
123-
124-
def __init__(self) -> None:
125-
self._buffer = bytearray()
126-
self._transport: serial_asyncio.SerialTransport | None = None
127-
self._connected_event = asyncio.Event()
128-
129-
async def wait_until_connected(self) -> None:
130-
"""Wait for the protocol's transport to be connected."""
131-
await self._connected_event.wait()
132-
133-
def connection_made(self, transport: serial_asyncio.SerialTransport) -> None:
134-
_LOGGER.debug("Connection made: %s", transport)
135-
136-
self._transport = transport
137-
self._connected_event.set()
138-
139-
def send_data(self, data: bytes) -> None:
140-
"""Sends data over the connected transport."""
141-
assert self._transport is not None
142-
data = bytes(data)
143-
_LOGGER.debug("Sending data %s", data)
144-
self._transport.write(data)
145-
146-
def data_received(self, data: bytes) -> None:
147-
_LOGGER.debug("Received data %s", data)
148-
self._buffer += data
149-
150-
def disconnect(self) -> None:
151-
if self._transport is not None:
152-
self._transport.close()
153-
self._buffer.clear()
154-
self._connected_event.clear()
155-
156-
157-
def patch_pyserial_asyncio() -> None:
158-
"""Patches pyserial-asyncio's `SerialTransport` to support swapping protocols."""
159-
160-
if (
161-
serial_asyncio.SerialTransport.get_protocol
162-
is not asyncio.BaseTransport.get_protocol
163-
):
164-
return
165-
166-
def get_protocol(self) -> asyncio.Protocol:
167-
return self._protocol
168-
169-
def set_protocol(self, protocol: asyncio.Protocol) -> None:
170-
self._protocol = protocol
171-
172-
serial_asyncio.SerialTransport.get_protocol = get_protocol
173-
serial_asyncio.SerialTransport.set_protocol = set_protocol
174-
175-
176120
@contextlib.asynccontextmanager
177121
async def connect_protocol(port, baudrate, factory):
178122
loop = asyncio.get_running_loop()
@@ -189,10 +133,7 @@ async def connect_protocol(port, baudrate, factory):
189133
try:
190134
yield protocol
191135
finally:
192-
protocol.disconnect()
193-
194-
# Required for Windows to be able to re-connect to the same serial port
195-
await asyncio.sleep(0)
136+
await protocol.disconnect()
196137

197138

198139
class CommaSeparatedNumbers(click.ParamType):

universal_silabs_flasher/cpc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import typing
77

88
import async_timeout
9+
from zigpy.serial import SerialProtocol
910
import zigpy.types
1011

1112
from . import cpc_types
12-
from .common import BufferTooShort, SerialProtocol, Version, crc16_ccitt
13+
from .common import BufferTooShort, Version, crc16_ccitt
1314

1415
_LOGGER = logging.getLogger(__name__)
1516

@@ -209,6 +210,8 @@ def poll_final(self) -> bool:
209210
class CPCProtocol(SerialProtocol):
210211
"""Partial implementation of the CPC protocol."""
211212

213+
_buffer: bytearray
214+
212215
def __init__(self) -> None:
213216
super().__init__()
214217
self._command_seq: int = 0
@@ -279,6 +282,11 @@ async def get_secondary_version(self) -> Version | None:
279282

280283
return Version(version_bytes.split(b"\x00", 1)[0].decode("ascii"))
281284

285+
def send_data(self, data: bytes) -> None:
286+
assert self._transport is not None
287+
_LOGGER.debug("Sending data %s", data)
288+
self._transport.write(data)
289+
282290
def data_received(self, data: bytes) -> None:
283291
super().data_received(data)
284292

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,32 @@
1-
import asyncio
21
import contextlib
32

43
import bellows.config
54
import bellows.ezsp
65
import bellows.types
6+
from bellows.zigbee.application import ControllerApplication
77
import zigpy.config
88

9-
AFTER_DISCONNECT_DELAY = 0.1
10-
119

1210
@contextlib.asynccontextmanager
1311
async def connect_ezsp(port: str, baudrate: int = 115200) -> bellows.ezsp.EZSP:
1412
"""Context manager to return a connected EZSP instance for a serial port."""
15-
app_config = zigpy.config.CONFIG_SCHEMA(
16-
{
17-
zigpy.config.CONF_DEVICE: {
18-
zigpy.config.CONF_DEVICE_PATH: port,
19-
zigpy.config.CONF_DEVICE_BAUDRATE: baudrate,
20-
},
21-
bellows.config.CONF_EZSP_CONFIG: {
22-
# Do not set any configuration on startup
23-
"CONFIG_END_DEVICE_POLL_TIMEOUT": None,
24-
"CONFIG_INDIRECT_TRANSMISSION_TIMEOUT": None,
25-
"CONFIG_TC_REJOINS_USING_WELL_KNOWN_KEY_TIMEOUT_S": None,
26-
"CONFIG_SECURITY_LEVEL": None,
27-
"CONFIG_APPLICATION_ZDO_FLAGS": None,
28-
"CONFIG_SUPPORTED_NETWORKS": None,
29-
"CONFIG_PAN_ID_CONFLICT_REPORT_THRESHOLD": None,
30-
"CONFIG_TRUST_CENTER_ADDRESS_CACHE_SIZE": None,
31-
"CONFIG_SOURCE_ROUTE_TABLE_SIZE": None,
32-
"CONFIG_MULTICAST_TABLE_SIZE": None,
33-
"CONFIG_ADDRESS_TABLE_SIZE": None,
34-
"CONFIG_PACKET_BUFFER_COUNT": None,
35-
"CONFIG_STACK_PROFILE": None,
36-
},
37-
bellows.config.CONF_USE_THREAD: False,
38-
}
13+
14+
ezsp = bellows.ezsp.EZSP(
15+
# We use this roundabout way to construct the device schema to make sure that
16+
# we are compatible with future changes to the zigpy device config schema.
17+
ControllerApplication.SCHEMA(
18+
{
19+
zigpy.config.CONF_DEVICE: {
20+
zigpy.config.CONF_DEVICE_PATH: port,
21+
zigpy.config.CONF_DEVICE_BAUDRATE: baudrate,
22+
}
23+
}
24+
)[zigpy.config.CONF_DEVICE]
3925
)
4026

41-
ezsp = await bellows.ezsp.EZSP.initialize(app_config)
27+
await ezsp.connect(use_thread=False)
4228

4329
try:
4430
yield ezsp
4531
finally:
46-
ezsp.close()
47-
await asyncio.sleep(AFTER_DISCONNECT_DELAY)
32+
await ezsp.disconnect()

universal_silabs_flasher/flash.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import zigpy.ota.validators
1818
import zigpy.types
1919

20-
from .common import CommaSeparatedNumbers, patch_pyserial_asyncio, put_first
20+
from .common import CommaSeparatedNumbers, put_first
2121
from .const import (
2222
DEFAULT_BAUDRATES,
2323
FW_IMAGE_TYPE_TO_APPLICATION_TYPE,
@@ -28,8 +28,6 @@
2828
from .flasher import Flasher
2929
from .xmodemcrc import BLOCK_SIZE as XMODEM_BLOCK_SIZE, ReceiverCancelled
3030

31-
patch_pyserial_asyncio()
32-
3331
_LOGGER = logging.getLogger(__name__)
3432
LOG_LEVELS = ["INFO", "DEBUG"]
3533

universal_silabs_flasher/flasher.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99
import bellows.config
1010
import bellows.ezsp
1111
import bellows.types
12+
from zigpy.serial import SerialProtocol
1213

13-
from .common import (
14-
PROBE_TIMEOUT,
15-
SerialProtocol,
16-
Version,
17-
connect_protocol,
18-
pad_to_multiple,
19-
)
14+
from .common import PROBE_TIMEOUT, Version, connect_protocol, pad_to_multiple
2015
from .const import DEFAULT_BAUDRATES, GPIO_CONFIGS, ApplicationType, ResetTarget
2116
from .cpc import CPCProtocol
2217
from .emberznet import connect_ezsp
@@ -115,8 +110,6 @@ async def probe_gecko_bootloader(
115110
if run_firmware:
116111
await gecko.run_firmware()
117112
_LOGGER.info("Launched application from bootloader")
118-
119-
await asyncio.sleep(1)
120113
except NoFirmwareError:
121114
_LOGGER.warning("No application can be launched")
122115
return ProbeResult(

universal_silabs_flasher/gecko_bootloader.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import typing
88

99
import async_timeout
10+
from zigpy.serial import SerialProtocol
1011

11-
from .common import PROBE_TIMEOUT, SerialProtocol, StateMachine, Version
12+
from .common import PROBE_TIMEOUT, StateMachine, Version
1213
from .xmodemcrc import send_xmodem128_crc
1314

1415
_LOGGER = logging.getLogger(__name__)
@@ -142,6 +143,11 @@ async def upload_firmware(
142143
if self._upload_status != "complete":
143144
raise UploadError(self._upload_status)
144145

146+
def send_data(self, data: bytes) -> None:
147+
assert self._transport is not None
148+
_LOGGER.debug("Sending data %s", data)
149+
self._transport.write(data)
150+
145151
def data_received(self, data: bytes) -> None:
146152
super().data_received(data)
147153

universal_silabs_flasher/spinel.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import typing
77

88
import async_timeout
9+
from zigpy.serial import SerialProtocol
910
import zigpy.types
1011

11-
from .common import SerialProtocol, Version, crc16_kermit
12+
from .common import Version, crc16_kermit
1213
from .spinel_types import CommandID, HDLCSpecial, PropertyID, ResetReason
1314

1415
_LOGGER = logging.getLogger(__name__)
@@ -104,11 +105,18 @@ def serialize(self) -> bytes:
104105

105106

106107
class SpinelProtocol(SerialProtocol):
108+
_buffer: bytearray
109+
107110
def __init__(self) -> None:
108111
super().__init__()
109112
self._transaction_id: int = 1
110113
self._pending_frames: dict[int, asyncio.Future] = {}
111114

115+
def send_data(self, data: bytes) -> None:
116+
assert self._transport is not None
117+
_LOGGER.debug("Sending data %s", data)
118+
self._transport.write(data)
119+
112120
def data_received(self, data: bytes) -> None:
113121
super().data_received(data)
114122

@@ -260,6 +268,3 @@ async def enter_bootloader(self) -> None:
260268
ResetReason.BOOTLOADER.serialize(),
261269
wait_response=False,
262270
)
263-
264-
# A small delay is necessary when switching baudrates
265-
await asyncio.sleep(0.5)

0 commit comments

Comments
 (0)