Replace progpib.py stub with a proper installable package

Builds a modern, testable API for the Prologix GPIB-ETHERNET controller
(GPIB-USB structurally supported but untested, no hardware available):
Transport abstraction (Ethernet now, serial later), GpibController for
the full "++" command set, a clean-room netfinder discovery client that
fixes a real chr()/struct-format bug present in vendortools/nfutil.py,
and a CLI (discover/config/info/terminal). vendortools/, pty-gpib-emulator/,
sampledata.txt and the manual PDF are kept untouched as reference material.

Depends on the sibling bits package (editable, not yet pinned -- see its
own commit) for MAC formatting and IEEE-488.2 status-byte bit access.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 18:43:08 -05:00
parent 6664760477
commit a9a79b6dd7
19 changed files with 1723 additions and 54 deletions

84
tests/test_controller.py Normal file
View File

@@ -0,0 +1,84 @@
import time
import pytest
from bits import Bit
from progpib.controller import GpibController
from progpib.exceptions import CommandError
def _wait_until(condition, timeout=2.0, interval=0.02):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if condition():
return True
time.sleep(interval)
return condition()
def test_mode_property_roundtrip(fake_gpib_server):
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
ctrl.mode = 1
assert ctrl.mode == 1
ctrl.mode = 0
assert ctrl.mode == 0
def test_addr_property_roundtrip_as_tuple(fake_gpib_server):
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
ctrl.addr = (5, 96)
assert ctrl.addr == (5, 96)
ctrl.addr = 12
assert ctrl.addr == 12
def test_ver_returns_server_version(fake_gpib_server):
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
assert ctrl.ver() == server.version
def test_write_escapes_special_bytes_on_the_wire(fake_gpib_server):
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
ctrl.write("*IDN?\r\n+test")
expected = b"*IDN?\x1b\r\x1b\n\x1b+test\n"
assert _wait_until(lambda: bytes(server.raw_received) == expected)
def test_read_tmo_ms_range_validation_without_network(fake_gpib_server):
server, tcp_server = fake_gpib_server
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
with pytest.raises(ValueError):
ctrl.read_tmo_ms = 5000
# unchanged: the invalid value was never sent
assert ctrl.read_tmo_ms == 500
def test_status_and_lon_raise_in_controller_mode(fake_gpib_server):
server, tcp_server = fake_gpib_server
server.state["mode"] = "1" # CONTROLLER
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
with pytest.raises(CommandError):
_ = ctrl.status
with pytest.raises(CommandError):
_ = ctrl.lon
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
server.state["status"] = "72" # 0x48: RQS (bit 6) and bit 3 set
server.spoll_response = 72
with GpibController.ethernet(tcp_server.host, tcp_server.port) as ctrl:
status = ctrl.status
assert int(status) == 72
assert status[6] == Bit(1)
assert status[5] == Bit(0)
result = ctrl.spoll()
assert int(result) == 72
assert result[6] == Bit(1)