fix: compute instrument read timeout before sending the read command

The previous fix computed the host-side wait (via a read_tmo_ms query)
after already sending "++read eoi" -- but the controller doesn't answer
any further command while that read is outstanding, so the read_tmo_ms
query itself raced the read it was meant to be timing and could time
out. Reordered so the timeout is resolved first in both query() and
read(); added a regression test pinning the wire ordering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 18:55:26 -05:00
parent b35864656a
commit 97a632435d
2 changed files with 26 additions and 2 deletions

View File

@@ -275,8 +275,13 @@ class GpibController:
if not (0 <= char <= 255):
raise ValueError("terminator char must be between 0 and 255")
command = f"read {char}"
# Compute the timeout (may itself query read_tmo_ms) *before* sending
# the read command: once issued, the controller won't answer any
# further command until it completes, so querying read_tmo_ms after
# would race the very read it's trying to time.
wait = self._instrument_read_timeout(timeout)
self._send_command(command)
return self._transport.read_until_quiet(timeout=self._instrument_read_timeout(timeout))
return self._transport.read_until_quiet(timeout=wait)
# -- raw instrument I/O ---------------------------------------------
@@ -295,5 +300,9 @@ class GpibController:
`vendortools/arb_eth.py` (`++auto 0` + explicit `++read`).
"""
self.write(data)
# Compute the timeout (may itself query read_tmo_ms) before sending
# "++read eoi" -- see the comment in read() for why the ordering
# matters.
wait = self._instrument_read_timeout(read_timeout)
self._send_command("read eoi")
return self._transport.read_until_quiet(timeout=self._instrument_read_timeout(read_timeout))
return self._transport.read_until_quiet(timeout=wait)

View File

@@ -67,6 +67,21 @@ def test_status_and_lon_raise_in_controller_mode(fake_gpib_server):
_ = ctrl.lon
def test_query_fetches_read_tmo_ms_before_sending_read_eoi(fake_gpib_server):
# Regression test: found against real hardware that the controller
# doesn't answer any further command while a "++read" it issued is
# still outstanding (waiting for EOI or its own read_tmo_ms timeout).
# So query()/read() must fetch read_tmo_ms (to size the host-side wait)
# *before* sending "++read eoi" -- fetching it after races the read
# it's meant to be timing. Assert that ordering on the wire.
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
ctrl.query("*IDN?", read_timeout=None)
assert _wait_until(lambda: b"++read eoi" in server.raw_received)
raw = bytes(server.raw_received)
assert raw.index(b"++read_tmo_ms") < raw.index(b"++read eoi")
def test_status_and_spoll_return_bits_with_rqs_at_index_6(fake_gpib_server):
server, tcp_server = fake_gpib_server
server.state["mode"] = "0" # DEVICE