From b35864656a2ebb7262345025260e957550e8bcd0 Mon Sep 17 00:00:00 2001 From: S Groesz Date: Mon, 24 Aug 2026 18:52:41 -0500 Subject: [PATCH] fix: tie query()/read() host timeout to the controller's read_tmo_ms Found against real hardware: a fixed 2.0s host-side read timeout was shorter than the device's configured read_tmo_ms (3000ms). The controller keeps a "++read" outstanding internally until EOI or its own timeout, so the client gave up and sent the next command first -- that command's reply then got lost/delayed behind the still-pending read, surfacing as a spurious TransportTimeout on the *following* call (observed: query("*IDN?") timed out as expected with no instrument attached, then srq() right after it timed out too, on a device with read_tmo_ms=3000). Co-Authored-By: Claude Sonnet 5 --- progpib/controller.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/progpib/controller.py b/progpib/controller.py index 470b624..1966d0c 100644 --- a/progpib/controller.py +++ b/progpib/controller.py @@ -7,7 +7,7 @@ from .exceptions import CommandError from .transport import EthernetTransport, SerialTransport, Transport _DEFAULT_COMMAND_TIMEOUT = 1.0 # seconds, for ++cmd query replies -_DEFAULT_READ_TIMEOUT = 2.0 # seconds, for ++read / query() instrument data +_READ_TIMEOUT_MARGIN = 0.5 # seconds, added on top of read_tmo_ms (see _instrument_read_timeout) class GpibController: @@ -59,6 +59,22 @@ class GpibController: if self.mode != 0: raise CommandError(f"++{command_name} is only valid in DEVICE mode") + def _instrument_read_timeout(self, explicit: float | None) -> float: + """Host-side timeout for reading instrument data. + + Must be longer than the controller's own `read_tmo_ms`: the + controller keeps a "++read" outstanding internally (waiting for + EOI or its own timeout) before it will process the next command, + so a shorter host-side timeout gives up and sends the next command + while the controller is still finishing the previous read -- that + reply then arrives late and gets mistaken for the next command's + reply (or times out entirely). Costs one extra round trip when the + caller doesn't pass an explicit timeout; pass one to skip it. + """ + if explicit is not None: + return explicit + return (self.read_tmo_ms / 1000.0) + _READ_TIMEOUT_MARGIN + # -- config properties ------------------------------------------------ @property @@ -260,7 +276,7 @@ class GpibController: raise ValueError("terminator char must be between 0 and 255") command = f"read {char}" self._send_command(command) - return self._transport.read_until_quiet(timeout=timeout or _DEFAULT_READ_TIMEOUT) + return self._transport.read_until_quiet(timeout=self._instrument_read_timeout(timeout)) # -- raw instrument I/O --------------------------------------------- @@ -280,4 +296,4 @@ class GpibController: """ self.write(data) self._send_command("read eoi") - return self._transport.read_until_quiet(timeout=read_timeout or _DEFAULT_READ_TIMEOUT) + return self._transport.read_until_quiet(timeout=self._instrument_read_timeout(read_timeout))