Skip to main content

Transports

A transport owns one connection to one instrument: opening it, closing it, and serializing I/O against it. It knows nothing about the instrument’s command vocabulary. SCPI syntax, register maps, and error-queue polling belong to the instrument driver that composes the transport. Concrete instrument drivers compose a transport in their constructor rather than extending it. Every transport inherits TransportBase, so the lifecycle, locking, and shared-ownership behavior on this page is identical no matter which one a driver holds.

Available transports

  • VisaDriver: VISA-attached instruments over GPIB, USB-TMC, TCP/IP (SOCKET, VXI-11, HiSLIP), and RS-232/RS-485. The transport every shipped SCPI driver sits on.
  • ModbusDriver: Modbus TCP and RTU, with raw function-code access and typed register encode and decode.
For a protocol neither one covers, implement a transport by subclassing TransportBase.

Lifecycle

Every transport follows the same four steps: open() and close() are both safe to call more than once. A best-effort teardown also runs on garbage collection, bypassing the shared-ownership guard, but you should not rely on this. Close explicitly in a try/finally, or wrap the transport in the open()/close() of a higher-level instrument driver.

Atomic multi-step sequences

Every transport is thread-safe at the I/O level. Each I/O call takes an internal reentrant lock for the duration of the call, so concurrent operations against the same transport are serialized rather than interleaved on the wire. A background poller and user code can therefore share one connection safely. When several operations need to execute atomically (a write followed by an error-queue check, a bank-select followed by a read, or any configuration sequence that must not be interrupted by another thread), use lock() as a context manager:
The lock is reentrant, so calling an I/O method from inside the with block does not deadlock the calling thread. Other threads still wait until the outer with exits.

Shared ownership

Some instruments expose more than one logical surface over a single connection. The EA PSB series both sources and sinks current on the same box, so it needs a PSU-shaped driver and an ELoad-shaped driver over one connection. Model this as one device class that owns the connection and vends one driver per category:
Each view implements one category contract and delegates its lifecycle to the device:
Ownership is two-level. The device holds the transport, and the device tracks its views itself. That keeps each “last release” unambiguous: the device’s last view leaving is what triggers device teardown, and the transport’s last holder leaving is what closes the connection. Because the device does its own teardown before calling close, the session is still up when SYST:LOCK OFF goes out, and no teardown callback has to be threaded through the transport for a close site to forget. transport.open(device) returns True only for the first owner, so a transport shared by several devices opens once. transport.close(device) tears the connection down only when the last device leaves. Construct the device once and take a view for each instrument:
Two driver classes rather than one class inheriting both contracts, because colliding methods need different bodies. get_current is positive out of the supply for InstroPSU and positive into the load for InstroELoad, reading the same meter. A driver that serves a single category owns its transport outright and never passes a holder. It is the sole owner by construction, so bare open()/close() behave exactly as in Lifecycle.

Implementing a transport

instro ships VisaDriver and ModbusDriver, and EtherNet/IP, OPC UA, and raw socket transports are planned, so check whether one already covers your protocol before writing your own. When none does, TransportBase is the only class a new transport subclasses: implement three members and everything above on this page comes from the base.

What TransportBase provides

The contract

Subclass TransportBase, call super().__init__() first, and implement three members: super().__init__() initializes the holder list and the lock. Skipping it leaves both uninitialized.
_open_session and _teardown_session are stable, supported extension points for TransportBase subclass authors. The leading underscore marks them as protected (implement them, do not call them), which is standard Python: callers drive the connection through the public open() and close(), and the base calls the hooks at the right moment. Both are documented on the transports reference page.
TransportBase is an abstract base class, so a missing member fails at construction rather than at the first I/O call: