PySerial
PySerialDocs

SCPI over Serial

Send SCPI commands over RS-232 serial with PySerial. Connect to bench instruments, query measurements, and handle errors.

Many bench instruments (multimeters, power supplies, oscilloscopes, signal generators) accept SCPI commands over RS-232. You send an ASCII string like *IDN? and read back the response.

Serial Settings for SCPI Instruments

SCPI over RS-232 varies by manufacturer. Check your instrument's manual for exact settings. These are the most common:

ParameterTypical valueNotes
Baud rate9600 or 19200Some instruments support 115200
Data bits8Always 8 for SCPI
ParityNoneSome HP/Agilent instruments use odd parity
Stop bits1Rarely 2
Flow controlNone or RTS/CTSUse RTS/CTS for instruments that support it
Termination\n (LF)Some use \r\n (CRLF) or \r (CR)

Connect and Identify

import serial

ser = serial.Serial(
    port="/dev/ttyUSB0",
    baudrate=9600,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=2,
)

ser.write(b"*IDN?\n")
identity = ser.readline().decode().strip()
print(identity)  # e.g. "Keysight Technologies,34461A,MY12345678,A.03.01"
ser.close()

The *IDN? query is standard SCPI. Every compliant instrument responds with manufacturer, model, serial number, and firmware version.

Log instrument measurements automatically

TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

Send and Query Pattern

SCPI has two types of messages: commands (no response) and queries (end with ?, expect a response).

import serial

def scpi_command(ser, cmd):
    """Send a SCPI command (no response expected)."""
    ser.write(f"{cmd}\n".encode())

def scpi_query(ser, cmd):
    """Send a SCPI query and return the response string."""
    ser.write(f"{cmd}\n".encode())
    return ser.readline().decode().strip()

ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=2)

# Configure a DC voltage measurement
scpi_command(ser, "CONF:VOLT:DC 10")  # 10V range
scpi_command(ser, "TRIG:SOUR IMM")     # Immediate trigger

# Take a measurement
voltage = scpi_query(ser, "MEAS:VOLT:DC?")
print(f"DC Voltage: {voltage} V")

# Read current
current = scpi_query(ser, "MEAS:CURR:DC?")
print(f"DC Current: {current} A")

ser.close()

Error Checking

SCPI instruments maintain an error queue. After a sequence of commands, query SYST:ERR? to check for problems.

import serial

def check_errors(ser):
    """Read all errors from the instrument's error queue."""
    errors = []
    while True:
        ser.write(b"SYST:ERR?\n")
        response = ser.readline().decode().strip()
        # Format: error_code,"error message"
        # No error returns: +0,"No error"
        if response.startswith("+0") or response.startswith("0"):
            break
        errors.append(response)
    return errors

ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=2)

# Send some commands
ser.write(b"CONF:VOLT:DC 10\n")
ser.write(b"BOGUS:COMMAND\n")  # This will cause an error

# Check for errors
errors = check_errors(ser)
if errors:
    for err in errors:
        print(f"Instrument error: {err}")
else:
    print("No errors")

ser.close()

Reset and Self-Test

Standard SCPI commands every instrument supports:

import serial
import time

ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=10)

# Reset to factory defaults
ser.write(b"*RST\n")
time.sleep(2)  # Give the instrument time to reset

# Run self-test (0 = pass)
ser.write(b"*TST?\n")
result = ser.readline().decode().strip()
print(f"Self-test: {'PASS' if result == '0' else 'FAIL'}")

# Wait for all pending operations to complete
ser.write(b"*OPC?\n")
opc = ser.readline().decode().strip()
print(f"Operations complete: {opc}")

ser.close()

Multiple Measurements

Read a batch of measurements in a loop. Add *OPC? synchronization if your instrument processes commands asynchronously.

import serial
import time

ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=5)

# Configure for DC voltage
ser.write(b"CONF:VOLT:DC AUTO\n")

readings = []
for i in range(10):
    ser.write(b"READ?\n")
    value = ser.readline().decode().strip()
    try:
        readings.append(float(value))
        print(f"Reading {i+1}: {value} V")
    except ValueError:
        print(f"Reading {i+1}: invalid response '{value}'")
    time.sleep(0.5)

if readings:
    avg = sum(readings) / len(readings)
    print(f"\nAverage: {avg:.6f} V")
    print(f"Min: {min(readings):.6f} V")
    print(f"Max: {max(readings):.6f} V")

ser.close()

Termination Characters

The most common source of SCPI-over-serial problems is wrong line termination. If readline() hangs, the instrument isn't sending the character PySerial expects.

Instrument familyTypical termination
Keysight/Agilent\n (LF)
Tektronix\n (LF)
Rohde & Schwarz\n (LF)
Keithley\r\n (CRLF)
older HP\r (CR) or \r\n

If readline() times out, try reading raw bytes to see what the instrument actually sends:

import serial

ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=3)
ser.write(b"*IDN?\n")
raw = ser.read(200)
print(repr(raw))  # Shows exact bytes including \r, \n
ser.close()

PyVISA vs Raw PySerial for SCPI

Both work. Here's when to pick which:

PySerialPyVISA
Installpip install pyserialpip install pyvisa pyvisa-py (or NI-VISA)
VISA resource stringN/AASRL/dev/ttyUSB0::INSTR
Termination handlingManual (\n in your strings)Automatic (read_termination property)
GPIB/USB-TMC/LANNoYes
Multiple transport backendsSerial onlySerial, GPIB, USB, Ethernet, all through one API
OverheadMinimalSlightly more (VISA layer)

Use PySerial when RS-232 is your only interface and you want minimal dependencies.

Use PyVISA when you also need GPIB, USB-TMC, or LAN instruments, or you want read_termination/write_termination to handle line endings for you. PyVISA uses PySerial under the hood for ASRL resources (when using the pyvisa-py backend).

import pyvisa

rm = pyvisa.ResourceManager("@py")  # pyvisa-py backend (uses PySerial)
inst = rm.open_resource("ASRL/dev/ttyUSB0::INSTR")
inst.baud_rate = 9600
inst.read_termination = "\n"
inst.write_termination = "\n"
print(inst.query("*IDN?"))
inst.close()
rm.close()

For dedicated PyVISA documentation, see the PyVISA docs.