Skip to content
Instrument Control

SCPI Commands in Python for Test Engineers

A reference for SCPI commands in Python using PyVISA, covering measurement types, triggering, error handling, and OpenHTF integration with TofuPilot.

JJulien Buteau
beginner13 min readMarch 14, 2026

SCPI (Standard Commands for Programmable Instruments) is the command language that most modern test instruments speak. If your multimeter, power supply, or oscilloscope was made after 1990, it almost certainly supports SCPI. This guide covers the commands you'll use most in manufacturing test, how to send them from Python with PyVISA, and how to integrate them into production tests with OpenHTF and TofuPilot.

What Is SCPI

SCPI defines a standard set of text commands for controlling instruments. Instead of learning a different protocol for every vendor, you use the same command structure across Keysight, Rigol, Rohde & Schwarz, Tektronix, and others.

Commands follow a tree structure:

:MEASure :VOLTage :DC? → Measure DC voltage :AC? → Measure AC voltage :CURRent :DC? → Measure DC current :RESistance? → Measure resistance

The colon separates levels. A question mark means "query" (read a value). No question mark means "command" (set something).

Setup

install.sh
pip install pyvisa pyvisa-py

Connect to an instrument:

scpi/connect.py
import pyvisarm = pyvisa.ResourceManager("@py")  # Pure Python backenddmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")dmm.timeout = 5000  # 5 seconds# Every SCPI instrument responds to *IDN?idn = dmm.query("*IDN?")print(f"Connected: {idn.strip()}")

Common SCPI Commands

IEEE 488.2 Mandatory Commands

Every SCPI instrument supports these:

CommandPurposeReturns
*IDN?Identify instrumentManufacturer, model, serial, firmware
*RSTReset to factory defaultsNothing
*CLSClear status and error queueNothing
*OPC?Operation complete query"1" when done
*TST?Self-test0 = pass, nonzero = fail
*WAIWait for pending operationsNothing (blocks until done)

Always start a test with *RST and *CLS. This puts the instrument in a known state.

scpi/reset.py
dmm.write("*RST")dmm.write("*CLS")

Measurement Commands

Two patterns: shorthand and explicit.

Shorthand (configure + trigger + read in one command):

scpi/measure_shorthand.py
# DC voltage, auto-rangevoltage = float(dmm.query(":MEAS:VOLT:DC?"))# DC current, auto-rangecurrent = float(dmm.query(":MEAS:CURR:DC?"))# Resistance, auto-rangeresistance = float(dmm.query(":MEAS:RES?"))# AC voltageac_voltage = float(dmm.query(":MEAS:VOLT:AC?"))

Explicit (separate configure, trigger, read for more control):

scpi/measure_explicit.py
# Configure for DC voltage, 10V rangedmm.write(":CONF:VOLT:DC 10")# Trigger a measurementdmm.write(":INIT")# Wait for completiondmm.query("*OPC?")# Read the resultvoltage = float(dmm.query(":FETCH?"))

Use the explicit pattern when you need precise timing control or when measuring multiple channels in sequence.

PatternWhen to UseCommands
:MEAS:...?Quick single reading1 command
:CONF: + :INIT + :FETCH?Precise timing, multi-channel3+ commands
:CONF: + :READ?Configure once, read repeatedly2 commands

Power Supply Commands

scpi/power_supply.py
import pyvisaimport timerm = pyvisa.ResourceManager("@py")psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")psu.timeout = 5000psu.write("*RST")psu.write("*CLS")# Select channel and configurepsu.write(":INST:SEL CH1")psu.write(":VOLT 5.0")          # Set 5V outputpsu.write(":CURR 0.5")          # 500mA current limit# Enable outputpsu.write(":OUTP ON")time.sleep(0.5)                  # Wait for stabilization# Read actual valuesactual_v = float(psu.query(":MEAS:VOLT?"))actual_i = float(psu.query(":MEAS:CURR?"))print(f"Output: {actual_v:.3f}V, {actual_i:.4f}A")# Always disable output at the endpsu.write(":OUTP OFF")psu.close()

Common power supply commands:

CommandPurpose
:INST:SEL CH1Select output channel
:VOLT 5.0Set voltage
:CURR 0.5Set current limit
:OUTP ON / :OUTP OFFEnable/disable output
:MEAS:VOLT?Read actual output voltage
:MEAS:CURR?Read actual output current
:VOLT:PROT 6.0Set over-voltage protection
:CURR:PROT 1.0Set over-current protection

Oscilloscope Commands

scpi/oscilloscope.py
# Basic oscilloscope setupscope = rm.open_resource("TCPIP::192.168.1.102::INSTR")scope.timeout = 10000scope.write("*RST")scope.write(":CHAN1:DISP ON")           # Enable channel 1scope.write(":CHAN1:SCAL 1.0")          # 1V/divscope.write(":TIM:SCAL 0.001")         # 1ms/divscope.write(":TRIG:EDGE:SOUR CHAN1")    # Trigger on channel 1scope.write(":TRIG:EDGE:LEV 1.5")      # Trigger at 1.5V# Measure frequency and amplitudefrequency = float(scope.query(":MEAS:FREQ? CHAN1"))amplitude = float(scope.query(":MEAS:VAMP? CHAN1"))

Error Handling

Instruments report errors through a queue. Always drain the queue after a command sequence.

scpi/error_handling.py
def check_errors(instr) -> list[str]:    """Read all errors from the SCPI error queue."""    errors = []    while True:        err = instr.query(":SYST:ERR?").strip()        code = int(err.split(",")[0])        if code == 0:            break        errors.append(err)    return errors# After a sequence of commandserrors = check_errors(dmm)if errors:    for e in errors:        print(f"  Error: {e}")    raise RuntimeError(f"Instrument errors: {errors}")

Common SCPI error codes:

CodeMeaningTypical Cause
-100Command errorTypo in command string
-200Execution errorInvalid parameter value
-300Device-specific errorHardware issue
-400Query errorQuery without reading response
0No errorQueue empty

Triggering Models

SCPI defines several trigger sources. The right choice depends on your timing requirements.

Trigger SourceCommandUse Case
Immediate:TRIG:SOUR IMMDefault. Measures as soon as configured.
Bus:TRIG:SOUR BUSSoftware trigger with *TRG. Precise timing.
External:TRIG:SOUR EXTHardware trigger line. Synchronized with other instruments.
Timer:TRIG:SOUR TIMPeriodic measurements at fixed intervals.

Bus trigger example (precise timing control):

scpi/bus_trigger.py
dmm.write(":CONF:VOLT:DC 10")       # Configure rangedmm.write(":TRIG:SOUR BUS")         # Bus trigger modedmm.write(":INIT")                   # Arm the trigger system# ... do other setup ...dmm.write("*TRG")                   # Send the triggerdmm.query("*OPC?")                  # Wait for completionreading = float(dmm.query(":FETCH?"))

Vendor-Specific Differences

SCPI is a standard, but vendors add their own extensions. Common differences:

FeatureKeysightRigolRohde & Schwarz
Channel select:INST:SEL CH1:INST CH1:INST:SEL 1
Screenshot:DISP:DATA? PNG:DISP:DATA?:HCOP:DATA?
Error query:SYST:ERR?:SYST:ERR?:SYST:ERR:ALL?
Beeper:SYST:BEEP:SYST:BEEP:STAT ON:SYST:BEEP:IMM

Always check your instrument's programming manual for exact syntax. The core measurement commands (:MEAS:, :CONF:, :INIT, :FETCH?) are consistent across vendors.

Integration with OpenHTF and TofuPilot

Wrap SCPI commands in an OpenHTF Plug for production tests. The plug handles connection lifecycle, and measurements flow through OpenHTF into TofuPilot automatically.

scpi/openhtf_integration.py
import pyvisaimport openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import TofuPilotclass ScpiDmm(BasePlug):    """SCPI multimeter plug with automatic lifecycle."""    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")    def measure_dc_voltage(self, range_v: str = "AUTO") -> float:        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:        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:        self.instr.write(f":CONF:RES {range_ohm}")        return float(self.instr.query(":MEAS:RES?"))    def tearDown(self):        self.instr.close()@htf.measures(    htf.Measurement("voltage_3v3")    .in_range(3.2, 3.4)    .with_units(units.VOLT),)@htf.plug(dmm=ScpiDmm)def test_rail(test, dmm):    test.measurements.voltage_3v3 = dmm.measure_dc_voltage()def main():    test = htf.Test(test_rail, procedure_id="FCT-001", part_number="PCBA-100")    with TofuPilot(test):        test.execute(test_start=lambda: input("Serial: "))

Quick Reference

TaskSCPI Command
Identify*IDN?
Reset*RST
Clear errors*CLS
Measure DC voltage:MEAS:VOLT:DC?
Measure DC current:MEAS:CURR:DC?
Measure resistance:MEAS:RES?
Set voltage (PSU):VOLT 5.0
Set current limit (PSU):CURR 0.5
Output on/off (PSU):OUTP ON / :OUTP OFF
Read error:SYST:ERR?
Wait for complete*OPC?
Software trigger*TRG

More Guides

Put this guide into practice