Skip to content
Instrument Control

Getting Started with PyVISA

Learn how to connect to and control test instruments (DMMs, oscilloscopes, power supplies) from Python using PyVISA, with TofuPilot measurement logging.

JJulien Buteau
beginner10 min readMarch 14, 2026

PyVISA lets you control test instruments (multimeters, oscilloscopes, power supplies, signal generators) from Python using the same VISA protocol that LabVIEW and TestStand use. If your instrument has a USB, GPIB, Ethernet, or serial interface, you can talk to it with PyVISA. This guide covers setup, connection, SCPI commands, and integrating instrument measurements into a TofuPilot test.

Prerequisites

  • Python 3.8+
  • A test instrument with USB-TMC, GPIB, Ethernet (LXI), or serial interface
  • Optional: NI-VISA runtime (for GPIB and some USB instruments)

Installation

pyvisa_setup/install.sh
pip install pyvisa pyvisa-py

Two backends are available:

BackendInstallWhen to Use
pyvisa-pypip install pyvisa-pyPure Python. Works on Linux/macOS/Windows. Supports USB-TMC, Ethernet (TCP/IP), serial. No NI software needed.
NI-VISAInstall from ni.comRequired for GPIB. Also supports USB-TMC and Ethernet. Windows and Linux only.

Start with pyvisa-py. Switch to NI-VISA only if you need GPIB or encounter compatibility issues.

Finding Your Instruments

Every VISA instrument has a resource string that identifies it. PyVISA can discover connected instruments automatically.

pyvisa_setup/discover.py
import pyvisarm = pyvisa.ResourceManager("@py")  # Use pyvisa-py backend# rm = pyvisa.ResourceManager()     # Use NI-VISA backend# List all connected instrumentsresources = rm.list_resources()print(f"Found {len(resources)} instrument(s):")for r in resources:    print(f"  {r}")

Common resource string formats:

InterfaceResource StringExample
USB-TMCUSB0::VID::PID::SERIAL::INSTRUSB0::0x2A8D::0x1301::MY59001234::INSTR
Ethernet (LXI)TCPIP::IP::INSTRTCPIP::192.168.1.100::INSTR
GPIBGPIB0::ADDR::INSTRGPIB0::22::INSTR
SerialASRL/dev/ttyUSB0::INSTRASRL/dev/ttyUSB0::INSTR

Connecting to an Instrument

Once you have the resource string, open a connection and verify identity with the standard *IDN? query.

pyvisa_setup/connect.py
import pyvisarm = pyvisa.ResourceManager("@py")dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")# Set timeout (milliseconds)dmm.timeout = 5000# Query instrument identityidn = dmm.query("*IDN?")print(f"Connected to: {idn.strip()}")# Output: Keysight Technologies,34461A,MY59001234,A.03.01

The query() method sends a command and reads the response. For commands that don't return data, use write().

SCPI Command Basics

SCPI (Standard Commands for Programmable Instruments) is the language most modern instruments speak. Commands follow a tree structure.

Reading a DC Voltage

pyvisa_setup/measure_voltage.py
import pyvisarm = pyvisa.ResourceManager("@py")dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")dmm.timeout = 5000# Configure for DC voltage measurement, auto-rangedmm.write(":CONF:VOLT:DC AUTO")# Trigger a measurement and read the resultdmm.write(":INIT")voltage = float(dmm.query(":FETCH?"))print(f"Voltage: {voltage:.4f} V")dmm.close()rm.close()

Common SCPI Commands

CommandPurposeExample
*IDN?Identify instrumentReturns manufacturer, model, serial, firmware
*RSTReset to factory defaultsGood practice at test start
*CLSClear error queueClear any previous errors
*OPC?Operation complete queryReturns "1" when last command finishes
:CONF:VOLT:DCConfigure DC voltage:CONF:VOLT:DC 10 for 10V range
:CONF:CURR:DCConfigure DC current:CONF:CURR:DC AUTO for auto-range
:CONF:RESConfigure resistance:CONF:RES AUTO
:MEAS:VOLT:DC?Measure DC voltage (configure + trigger + read)Returns float
:INITTrigger measurementUse with :FETCH? for separate trigger/read
:FETCH?Read last measurementReturns float
:SYST:ERR?Read error queueReturns error code and message

The :MEAS: shorthand configures, triggers, and reads in one command. Use :CONF: + :INIT + :FETCH? when you need more control over timing.

Controlling a Power Supply

Power supplies use similar SCPI commands but add output control.

pyvisa_setup/power_supply.py
import pyvisaimport timerm = pyvisa.ResourceManager("@py")psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")psu.timeout = 5000# Reset to known statepsu.write("*RST")psu.write("*CLS")# Configure channel 1: 5V, 500mA current limitpsu.write(":INST:SEL CH1")psu.write(":VOLT 5.0")psu.write(":CURR 0.5")# Enable outputpsu.write(":OUTP ON")time.sleep(0.5)  # Wait for output to stabilize# Read actual voltage and currentactual_voltage = float(psu.query(":MEAS:VOLT?"))actual_current = float(psu.query(":MEAS:CURR?"))print(f"Output: {actual_voltage:.3f}V, {actual_current:.4f}A")# Disable outputpsu.write(":OUTP OFF")psu.close()rm.close()

Error Handling

Instruments communicate errors through the SCPI error queue. Always check for errors after a sequence of commands.

pyvisa_setup/error_handling.py
import pyvisadef check_instrument_errors(instr):    """Read and report all errors from the instrument error queue."""    errors = []    while True:        err = instr.query(":SYST:ERR?").strip()        code = int(err.split(",")[0])        if code == 0:            break        errors.append(err)    return errorsrm = pyvisa.ResourceManager("@py")dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")dmm.timeout = 5000# Send commandsdmm.write("*RST")dmm.write(":CONF:VOLT:DC AUTO")dmm.write(":INIT")voltage = float(dmm.query(":FETCH?"))# Check for errorserrors = check_instrument_errors(dmm)if errors:    print(f"Instrument errors: {errors}")else:    print(f"Voltage: {voltage:.4f} V (no errors)")dmm.close()

Integrating with OpenHTF and TofuPilot

In a production test, instrument control lives inside an OpenHTF Plug. The plug manages the connection lifecycle, and measurements flow through OpenHTF into TofuPilot automatically.

pyvisa_setup/production_test.py
import pyvisaimport openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import TofuPilotclass MultimeterPlug(BasePlug):    """PyVISA multimeter plug with automatic lifecycle management."""    RESOURCE = "TCPIP::192.168.1.100::INSTR"    def setUp(self):        rm = pyvisa.ResourceManager("@py")        self.instr = rm.open_resource(self.RESOURCE)        self.instr.timeout = 5000        self.instr.write("*RST")        self.instr.write("*CLS")        self.logger.info(f"Connected: {self.instr.query('*IDN?').strip()}")    def measure_dc_voltage(self, range_v: str = "AUTO") -> float:        """Take a single DC voltage measurement."""        self.instr.write(f":CONF:VOLT:DC {range_v}")        return float(self.instr.query(":MEAS:VOLT:DC?"))    def measure_dc_current(self, range_a: str = "AUTO") -> float:        """Take a single DC current measurement."""        self.instr.write(f":CONF:CURR:DC {range_a}")        return float(self.instr.query(":MEAS:CURR:DC?"))    def measure_resistance(self, range_ohm: str = "AUTO") -> float:        """Take a single resistance measurement."""        self.instr.write(f":CONF:RES {range_ohm}")        return float(self.instr.query(":MEAS:RES?"))    def tearDown(self):        self.instr.close()        self.logger.info("Multimeter disconnected")@htf.measures(    htf.Measurement("supply_3v3")    .in_range(3.2, 3.4)    .with_units(units.VOLT)    .doc("3.3V rail voltage"),    htf.Measurement("supply_5v0")    .in_range(4.8, 5.2)    .with_units(units.VOLT)    .doc("5.0V rail voltage"),    htf.Measurement("idle_current")    .in_range(0.01, 0.15)    .with_units(units.AMPERE)    .doc("Board idle current draw"),)@htf.plug(dmm=MultimeterPlug)def test_power_rails(test, dmm):    """Measure all power rails and idle current."""    test.measurements.supply_3v3 = dmm.measure_dc_voltage()    test.measurements.supply_5v0 = dmm.measure_dc_voltage()    test.measurements.idle_current = dmm.measure_dc_current()@htf.measures(    htf.Measurement("pullup_resistance")    .in_range(4500, 5500)    .with_units(units.OHM)    .doc("I2C pullup resistance (expect 4.7k)"),)@htf.plug(dmm=MultimeterPlug)def test_pullup_resistors(test, dmm):    """Verify I2C pullup resistor values."""    test.measurements.pullup_resistance = dmm.measure_resistance()def main():    test = htf.Test(        test_power_rails,        test_pullup_resistors,        procedure_id="FCT-001",        part_number="PCBA-100",    )    with TofuPilot(test):        test.execute(test_start=lambda: input("Scan serial number: "))if __name__ == "__main__":    main()

Every measurement (voltage, current, resistance) flows into TofuPilot with its name, value, limits, and units. You get FPY tracking, control charts, and Cpk analysis on each measurement without extra code.

Troubleshooting

ProblemCauseFix
VisaIOError: VI_ERROR_RSRC_NFOUNDInstrument not foundCheck cable, verify IP/USB, run rm.list_resources()
VisaIOError: VI_ERROR_TMOTimeoutIncrease instr.timeout, check if instrument is busy
VisaIOError: VI_ERROR_CONN_LOSTConnection droppedCheck network, retry connection
Empty response from query()Instrument didn't respondAdd time.sleep() after write, check command syntax
USB instrument not detectedMissing driverInstall NI-VISA runtime or check USB-TMC kernel module (Linux)
ValueError: could not convert string to floatUnexpected response formatPrint raw response, check for error messages mixed in

More Guides

Put this guide into practice