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 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 18:52:41 -05:00
parent a9a79b6dd7
commit b35864656a

View File

@@ -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))