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:
open(view) opens the connection if needed and reports whether this call made the view the first owner, so one-time device setup (the box’s remote lock, here) runs exactly once however many views open. close(view, ...) mirrors this: the connection stays open as long as any view holds it, and only the close that empties the owner list runs on_last_release and tears the connection down. 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: