From db36727e9d299de88e55a32825a4a690ac602ecf Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:24:36 +0530 Subject: [PATCH 1/3] fix: reject SCPI responses that end before their newline pyserial's readline() returns whatever arrived before the timeout, so a response cut off mid-line ("12" of "123\n") was returned by query() as a complete answer. Require the line terminator in query() and in the text error line read by read_block(), and raise ScpiTimeoutError otherwise, which is what the Wi-Fi transport already does. --- pslab/pico/transport.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pslab/pico/transport.py b/pslab/pico/transport.py index ab9490c..c757355 100644 --- a/pslab/pico/transport.py +++ b/pslab/pico/transport.py @@ -233,7 +233,8 @@ def query(self, command: str) -> str: self._write_line(command) line = self.transport.readline() - if not line: + # pyserial returns a partial line without the terminator on timeout. + if not line.endswith(b"\n"): raise ScpiTimeoutError(f"Timed out waiting for response to {command!r}.") return line.decode("ascii", errors="replace").strip() @@ -248,9 +249,10 @@ def read_block(self) -> bytes: marker = self._read_exact(1) if marker != b"#": - rest = self.transport.readline() - message = (marker + rest).decode("ascii", errors="replace").strip() - raise ScpiError(message) + line = marker + self.transport.readline() + if not line.endswith(b"\n"): + raise ScpiTimeoutError("Timed out waiting for SCPI error response.") + raise ScpiError(line.decode("ascii", errors="replace").strip()) digit_count_text = self._read_exact(1) try: From a17dadb8c69bba2cea6fc163bbae4c724d380209 Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:24:56 +0530 Subject: [PATCH 2/3] fix: close the Wi-Fi socket when connecting fails PicoWifiTransport.connect() left the socket open if connect() raised (connection refused or timed out), leaking one socket per attempt against an unreachable bridge. --- pslab/pico/transport.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pslab/pico/transport.py b/pslab/pico/transport.py index c757355..3b7e423 100644 --- a/pslab/pico/transport.py +++ b/pslab/pico/transport.py @@ -156,8 +156,12 @@ def connect(self) -> None: if self._socket is not None: return sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self.timeout) - sock.connect((self.host, self.port)) + try: + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + except BaseException: + sock.close() + raise self._socket = sock def close(self) -> None: From 939c661c6e0f17ef056cb938e7f0170463ca2263 Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:25:36 +0530 Subject: [PATCH 3/3] test: cover truncated SCPI responses and failed Wi-Fi connects --- tests/test_pico_scpi_transport.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_pico_scpi_transport.py b/tests/test_pico_scpi_transport.py index dbed58f..af7d7c2 100644 --- a/tests/test_pico_scpi_transport.py +++ b/tests/test_pico_scpi_transport.py @@ -234,3 +234,41 @@ def test_wifi_readline_wraps_socket_timeout(): with pytest.raises(ScpiTimeoutError): transport.readline() + + +class TruncatingTransport(FakeTransport): + """Return a response without its newline, as pyserial does on timeout.""" + + def write(self, data): + self.output.extend(b"12") + return len(data) + + +def test_query_rejects_response_without_newline(): + client = ScpiClient(TruncatingTransport()) + + with pytest.raises(ScpiTimeoutError): + client.error_count() + + +def test_query_block_rejects_error_line_without_newline(): + client = ScpiClient(TruncatingTransport()) + + with pytest.raises(ScpiTimeoutError): + client.query_block("LA:READ?") + + +def test_wifi_connect_closes_socket_when_connect_fails(monkeypatch): + class RefusingSocket(FakeSocket): + def connect(self, address): + raise ConnectionRefusedError + + sock = RefusingSocket([]) + monkeypatch.setattr(socket, "socket", lambda *args: sock) + transport = PicoWifiTransport("example.test") + + with pytest.raises(ConnectionRefusedError): + transport.connect() + + assert sock.closed + assert not transport.is_open