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.

On this page

Deploy your PySerial test scripts with TofuPilot

Trusted by Lyft, Cobot, Pacific Defense, and more.

TofuPilot provides dashboards and analytics to track serial device testing at scale.

TofuPilot
Get Started