From bac720b1bbfcbfd71ac3820173cab60feb1f94c6 Mon Sep 17 00:00:00 2001 From: zigpy-review-bot <286747149+zigpy-review-bot@users.noreply.github.com> Date: Mon, 28 Sep 2026 01:49:24 +0200 Subject: [PATCH 1/2] Fix `flow_control: "software"` enabling RTS/CTS instead of XON/XOFF Since #595, `uart._connect` treated every non-`None` `flow_control` value as hardware flow control, so `"software"` opened the serial port with `rtscts=True`. This was harmless as long as the USB-serial driver ignored `CRTSCTS`, but Linux 6.14 added real RTS/CTS support to the `ch341` driver, and CH340-based adapters running software-flow-control firmware (e.g. Elelabs ELU013/ELR023) now time out during the ASH reset because the bridge waits for CTS. Map `"software"` to XON/XOFF and only enable RTS/CTS for `"hardware"`. `None` keeps XON/XOFF enabled, as before. Assert the resulting serial port flags in the UART connect test and fix the CLI help text. --- bellows/cli/opts.py | 5 ++++- bellows/uart.py | 12 ++++++------ tests/test_uart.py | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/bellows/cli/opts.py b/bellows/cli/opts.py index 31c8835e..18d8bd42 100644 --- a/bellows/cli/opts.py +++ b/bellows/cli/opts.py @@ -62,7 +62,10 @@ default="software", type=click.Choice(["hardware", "software", "none"], case_sensitive=False), envvar="EZSP_FLOW_CONTROL", - help="use hardware flow control", + help=( + "serial port flow control: hardware (RTS/CTS) or software (XON/XOFF);" + " none currently behaves like software" + ), ) pan = click.option("-P", "--pan-id", type=click.IntRange(0, 65535)) diff --git a/bellows/uart.py b/bellows/uart.py index af274dc8..f64764be 100644 --- a/bellows/uart.py +++ b/bellows/uart.py @@ -113,18 +113,18 @@ async def _connect(config, api): gateway = Gateway(api, connection_done_future) protocol = AshProtocol(gateway) - if config[zigpy.config.CONF_DEVICE_FLOW_CONTROL] is None: - xon_xoff, rtscts = True, False - else: - xon_xoff, rtscts = False, True + flow_control = config[zigpy.config.CONF_DEVICE_FLOW_CONTROL] + # `None` keeps XON/XOFF enabled, as before: ASH escapes 0x11/0x13 on the wire, so + # host-side XON/XOFF cannot corrupt framing, and it is the only thing honoring XOFF + # from software-flow-control firmware (which many installs run with `None` stored). transport, _ = await zigpy.serial.create_serial_connection( loop, lambda: protocol, url=config[zigpy.config.CONF_DEVICE_PATH], baudrate=config[zigpy.config.CONF_DEVICE_BAUDRATE], - xonxoff=xon_xoff, - rtscts=rtscts, + xonxoff=flow_control in (None, "software"), + rtscts=flow_control == "hardware", ) await gateway.wait_until_connected() diff --git a/tests/test_uart.py b/tests/test_uart.py index 7cc44b27..1bfda7c2 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -10,12 +10,21 @@ import bellows.types as t -@pytest.mark.parametrize("flow_control", ["software", "hardware"]) -async def test_connect(flow_control, monkeypatch): +@pytest.mark.parametrize( + ("flow_control", "xonxoff", "rtscts"), + [ + (None, True, False), + ("software", True, False), + ("hardware", False, True), + ], +) +async def test_connect(flow_control, xonxoff, rtscts, monkeypatch): appmock = MagicMock() transport = MagicMock() + connect_kwargs = {} async def mockconnect(loop, protocol_factory, **kwargs): + connect_kwargs.update(kwargs) protocol = protocol_factory() loop.call_soon(protocol.connection_made, transport) return None, protocol @@ -33,6 +42,9 @@ async def mockconnect(loop, protocol_factory, **kwargs): use_thread=False, ) + assert connect_kwargs["xonxoff"] is xonxoff + assert connect_kwargs["rtscts"] is rtscts + threads = [t for t in threading.enumerate() if "bellows" in t.name] assert len(threads) == 0 gw.close() From c57d68b707f9d50ddaeee35b2c8a6026c21862e2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:15:21 -0400 Subject: [PATCH 2/2] Remove unnecessary comment --- bellows/uart.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bellows/uart.py b/bellows/uart.py index f64764be..9849e0a6 100644 --- a/bellows/uart.py +++ b/bellows/uart.py @@ -115,9 +115,6 @@ async def _connect(config, api): flow_control = config[zigpy.config.CONF_DEVICE_FLOW_CONTROL] - # `None` keeps XON/XOFF enabled, as before: ASH escapes 0x11/0x13 on the wire, so - # host-side XON/XOFF cannot corrupt framing, and it is the only thing honoring XOFF - # from software-flow-control firmware (which many installs run with `None` stored). transport, _ = await zigpy.serial.create_serial_connection( loop, lambda: protocol,