Instrument Control Patterns
PyVISA patterns for resource management, error handling, and multi-instrument coordination in Python test scripts.
PyVISA connects Python to any VISA-compatible instrument over USB, Ethernet, GPIB, or RS-232. This page covers the patterns that keep your scripts reliable in production: resource cleanup, error recovery, and coordinating multiple instruments.
Resource Management
Leaked VISA sessions cause "resource busy" errors and eventually lock out the instrument. Always close what you open.
Context Managers
The cleanest approach. Resources close automatically, even if an exception fires.
import pyvisa
with pyvisa.ResourceManager() as rm:
with rm.open_resource("USB0::0x1234::0x5678::SN::INSTR") as inst:
inst.timeout = 5000
print(inst.query("*IDN?"))
# Both inst and rm are closed heretry/finally
Use this when you need the instrument handle across multiple functions or when context managers don't fit.
import pyvisa
rm = pyvisa.ResourceManager()
inst = rm.open_resource("USB0::0x1234::0x5678::SN::INSTR")
try:
inst.timeout = 5000
inst.write_termination = '\n'
inst.read_termination = '\n'
print(inst.query("*IDN?"))
finally:
inst.close()
rm.close()Log measurements automatically
TofuPilot records test results from your PyVISA scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
Error Handling
VISA Errors
PyVISA raises VisaIOError with a numeric error code. The two you'll hit most often are timeout (-1073807339) and invalid resource (-1073807343).
import pyvisa
from pyvisa import VisaIOError
rm = pyvisa.ResourceManager()
try:
inst = rm.open_resource("USB0::0x1234::0x5678::SN::INSTR")
inst.timeout = 5000
response = inst.query("*IDN?")
print(response.strip())
except VisaIOError as e:
if e.error_code == -1073807339:
print("Timeout: instrument not responding. Check cable and power.")
elif e.error_code == -1073807343:
print("Invalid resource string. Run rm.list_resources() to see what's available.")
else:
print(f"VISA error: {e}")
finally:
rm.close()SCPI Error Queue
Instruments queue SCPI errors internally. Drain the queue after a command sequence to catch problems early.
def drain_error_queue(inst):
"""Read all errors from the instrument's SCPI error queue."""
errors = []
while True:
err = inst.query("SYST:ERR?")
if err.startswith("+0") or err.startswith("0"):
break
errors.append(err.strip())
return errors
# After a sequence of writes
inst.write("CONF:VOLT:DC")
inst.write("VOLT:DC:NPLC 10")
errors = drain_error_queue(inst)
if errors:
raise RuntimeError(f"SCPI errors: {errors}")Multi-Instrument Coordination
Production test stations typically involve a power supply, a signal source, and one or more measurement instruments. The pattern below keeps setup, test, and teardown organized.
import pyvisa
import time
def run_dut_characterization():
rm = pyvisa.ResourceManager()
psu = rm.open_resource("ASRL/dev/ttyUSB0::INSTR")
psu.baud_rate = 115200
psu.timeout = 5000
siggen = rm.open_resource("USB0::0x2A8D::0x0001::MY52345678::INSTR")
siggen.timeout = 5000
scope = rm.open_resource("USB0::0x0699::0x0363::C065089::INSTR")
scope.timeout = 10000
try:
# Power up
psu.write("VOLT 5.0")
psu.write("CURR 1.0")
psu.write("OUTP ON")
time.sleep(1) # settling
# Inject test signal
siggen.write("SOUR:FUNC SIN")
siggen.write("SOUR:FREQ 1000")
siggen.write("SOUR:VOLT 1.0")
siggen.write("OUTP ON")
time.sleep(0.5)
# Measure response
scope.write("SING")
scope.query("*OPC?")
frequency = float(scope.query("MEASU:FREQ?"))
amplitude = float(scope.query("MEASU:PK2PK?"))
# Measure power draw
supply_v = float(psu.query("MEAS:VOLT?"))
supply_i = float(psu.query("MEAS:CURR?"))
results = {
"output_frequency_hz": frequency,
"output_amplitude_vpp": amplitude,
"power_consumption_w": supply_v * supply_i,
"passed": abs(frequency - 1000) < 10,
}
return results
finally:
siggen.write("OUTP OFF")
psu.write("OUTP OFF")
for r in [scope, siggen, psu]:
r.close()
rm.close()
results = run_dut_characterization()
for k, v in results.items():
print(f" {k}: {v}")Debugging
Enable PyVISA's built-in logging to see every VISA call on the wire.
import pyvisa
import logging
# Log to console
pyvisa.log_to_screen(logging.DEBUG)
# Or log to file
logging.basicConfig(
filename='pyvisa_debug.log',
level=logging.DEBUG,
format='%(asctime)s %(name)s %(levelname)s %(message)s'
)
rm = pyvisa.ResourceManager()
inst = rm.open_resource("USB0::0x1234::0x5678::SN::INSTR")
inst.query("*IDN?") # This call and its response will be loggedCommon Pitfalls
| Mistake | Consequence | Fix |
|---|---|---|
No inst.close() in error paths | "Resource busy" on next run | Use context managers or try/finally |
| Default 2 s timeout for slow commands | VisaIOError on calibration, self-test | Set inst.timeout per operation |
| Opening/closing in a loop | Slow, fragile | Open once, reuse the handle |
Ignoring SYST:ERR? | Silent wrong measurements | Drain the error queue after setup |
| Hardcoded resource strings | Breaks on different PCs | Use rm.list_resources() or config files |