Transports#
The connection layer instrument drivers compose, and how to implement a new one
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.ModbusTCPTransport/ModbusRTUTransport: Modbus TCP and RTU, with raw function-code access, typed register encode and decode, and per-callunit_idaddressing so multiple units share one line.
For a protocol neither one covers, implement a transport by subclassing TransportBase.
Lifecycle#
Every transport follows the same four steps:
Step |
What happens |
|---|---|
Construct |
Connection parameters are stored. No I/O happens yet. |
|
Opens the connection and applies the transport’s configuration. Idempotent. |
I/O methods |
Issue commands against the open connection. Each transport exposes the methods its protocol needs. |
|
Tears the connection down. Idempotent. Declines and logs a warning instead if another driver still holds the connection; see Shared ownership. |
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.
transport.open()
try:
... # I/O here
finally:
transport.close()
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:
with transport.lock():
transport.write("CONF:VOLT:DC")
transport.write("RANGE 10")
reading = transport.query("READ?")
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.
Implementing a transport#
instro ships VisaDriver and the Modbus transports (ModbusTCPTransport, ModbusRTUTransport), 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#
Member |
What the base provides |
|---|---|
|
Opens the connection through |
|
Tears the connection down through |
|
The reentrant lock guarding this connection, for callers that need a multi-step sequence to stay atomic. |
|
Best-effort teardown on garbage collection, bypassing the ownership guard and swallowing errors. A backstop against a stranded socket, not something to rely on. |
The contract#
Subclass TransportBase, call super().__init__() first, and implement three members:
Member |
Requirement |
|---|---|
|
Abstract property. |
|
Establish the connection. Must be idempotent: |
|
Release the connection. Must tolerate being called when already closed, because both |
super().__init__() initializes the holder list and the lock. Skipping it leaves both uninitialized.
Note
_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:
>>> Incomplete()
TypeError: Can't instantiate abstract class Incomplete without an implementation for abstract method 'is_open'