Hardware Testing with SCPI

Automate hardware tests using SCPI and log results with OptixLog

Hardware Testing with SCPI

Automate hardware tests using SCPI (Standard Commands for Programmable Instruments) and log results with OptixLog.

Overview

SCPI (Standard Commands for Programmable Instruments) is a standard protocol for controlling test equipment like:

  • Oscilloscopes (Keysight, Tektronix, Rigol)
  • Signal Generators (Agilent, Rohde & Schwarz)
  • Power Meters (Keysight, Anritsu)
  • Network Analyzers (Keysight, Anritsu)
  • Optical Test Equipment (EXFO, VIAVI)

OptixLog SDK can log all your hardware test results, making it easy to track measurements, compare test runs, and maintain test history.

Setup

Install Required Libraries

pip install optixlog pyvisa pyvisa-py
  • pyvisa — Python interface for VISA (Virtual Instrument Software Architecture)
  • pyvisa-py — Pure Python backend for pyvisa (no drivers needed)

Connect to Instruments

import pyvisa
from optixlog import Optixlog

# Initialize VISA resource manager
rm = pyvisa.ResourceManager()

# List available instruments
resources = rm.list_resources()
print(f"Available instruments: {resources}")

# Connect to instrument (example: USB, GPIB, or TCP/IP)
# USB: 'USB0::0x1234::0x5678::INSTR'
# GPIB: 'GPIB0::1::INSTR'
# TCP/IP: 'TCPIP0::192.168.1.100::inst0::INSTR'
instrument = rm.open_resource('USB0::0x1234::0x5678::INSTR')

Basic SCPI Communication

Sending Commands

# Write command (no response expected)
instrument.write('*RST')  # Reset instrument
instrument.write(':FREQ 1.55E9')  # Set frequency to 1.55 GHz

# Query command (response expected)
frequency = instrument.query(':FREQ?')  # Query frequency
print(f"Current frequency: {frequency} Hz")

Reading Measurements

# Single measurement
power = float(instrument.query(':MEAS:POW?'))
print(f"Power: {power} dBm")

# Multiple measurements
instrument.write(':INIT:CONT ON')  # Continuous measurement
for i in range(10):
    power = float(instrument.query(':MEAS:POW?'))
    print(f"Measurement {i}: {power} dBm")

Logging with OptixLog

Basic Power Measurement

import os
import time
import numpy as np
import matplotlib.pyplot as plt
import pyvisa
from optixlog import Optixlog

# Initialize OptixLog
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="HardwareTesting", create_if_not_exists=True)

# Create run with configuration
run = project.run(
    name="power_measurement",
    config={
        "instrument": "Keysight Power Meter",
        "wavelength": 1550,
        "unit": "dBm"
    }
)

# Connect to instrument
rm = pyvisa.ResourceManager()
instrument = rm.open_resource('USB0::0x1234::0x5678::INSTR')

# Initialize instrument
instrument.write('*RST')
instrument.write(':WAV 1550')  # Set wavelength to 1550 nm

# Take measurements
measurements = []
for step in range(100):
    power = float(instrument.query(':MEAS:POW?'))
    measurements.append(power)
    
    # Log to OptixLog
    run.log(step=step, power=power)
    time.sleep(0.1)

# Log measurement statistics
run.log(
    step=100,
    mean_power=np.mean(measurements),
    std_power=np.std(measurements),
    min_power=np.min(measurements),
    max_power=np.max(measurements)
)

# Create and log plot
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(range(len(measurements)), measurements, 'b-', linewidth=1)
ax.set_xlabel("Time (samples)")
ax.set_ylabel("Power (dBm)")
ax.set_title("Power vs Time")
ax.grid(True, alpha=0.3)
run.log_matplotlib("power_vs_time", fig)
plt.close(fig)

instrument.close()

Wavelength Sweep

import os
import time
import numpy as np
import matplotlib.pyplot as plt
import pyvisa
from optixlog import Optixlog

# Initialize OptixLog
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="HardwareTesting", create_if_not_exists=True)

wavelengths = np.linspace(1530, 1570, 100)  # 100 points from 1530-1570 nm

run = project.run(
    name="wavelength_sweep",
    config={
        "wavelength_range": [1530, 1570],
        "points": 100,
        "instrument": "Tunable Laser + Power Meter"
    }
)

# Connect to instruments
rm = pyvisa.ResourceManager()
power_meter = rm.open_resource('USB0::0x1234::0x5678::INSTR')
tunable_laser = rm.open_resource('USB0::0xABCD::0xEF01::INSTR')

transmission = []

for i, wavelength in enumerate(wavelengths):
    # Set laser wavelength
    tunable_laser.write(f':WAV {wavelength}')
    time.sleep(0.1)  # Wait for laser to stabilize
    
    # Measure power
    power = float(power_meter.query(':MEAS:POW?'))
    transmission.append(power)
    
    # Log each measurement
    run.log(
        step=i,
        wavelength=wavelength,
        power=power,
        transmission=power
    )

# Create and log spectrum plot
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(wavelengths, transmission, 'b-', linewidth=1)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Power (dBm)")
ax.set_title("Transmission Spectrum")
ax.grid(True, alpha=0.3)
run.log_matplotlib("transmission_spectrum", fig)
plt.close(fig)

# Log statistics
peak_idx = np.argmax(transmission)
run.log(
    step=len(wavelengths),
    peak_wavelength=wavelengths[peak_idx],
    peak_power=np.max(transmission)
)

power_meter.close()
tunable_laser.close()

Multi-Parameter Sweep

import os
import time
import numpy as np
import matplotlib.pyplot as plt
import pyvisa
from optixlog import Optixlog

# Initialize OptixLog
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="HardwareTesting", create_if_not_exists=True)

frequencies = np.linspace(1e9, 10e9, 50)  # 1-10 GHz
amplitudes = [-10, -5, 0, 5, 10]  # dBm

run = project.run(
    name="multi_parameter_sweep",
    config={
        "frequencies": list(frequencies),
        "amplitudes": amplitudes,
        "instruments": ["Signal Generator", "Oscilloscope"]
    }
)

# Connect to instruments
rm = pyvisa.ResourceManager()
signal_gen = rm.open_resource('GPIB0::1::INSTR')
oscilloscope = rm.open_resource('GPIB0::2::INSTR')

results = []

for freq_idx, frequency in enumerate(frequencies):
    for amp_idx, amplitude in enumerate(amplitudes):
        # Configure signal generator
        signal_gen.write(f':FREQ {frequency}')
        signal_gen.write(f':POW {amplitude}')
        signal_gen.write(':OUTP ON')
        time.sleep(0.1)
        
        # Measure on oscilloscope
        voltage = float(oscilloscope.query(':MEAS:VOLT:MAX?'))
        phase = float(oscilloscope.query(':MEAS:PHAS?'))
        
        # Log measurement
        step = freq_idx * len(amplitudes) + amp_idx
        run.log(
            step=step,
            frequency=frequency,
            amplitude=amplitude,
            voltage=voltage,
            phase=phase
        )
        
        results.append({
            'frequency': frequency,
            'amplitude': amplitude,
            'voltage': voltage,
            'phase': phase
        })

# Create and log 2D heatmap
voltage_matrix = np.array([r['voltage'] for r in results]).reshape(
    len(frequencies), len(amplitudes)
)

fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(voltage_matrix, aspect='auto', cmap='hot', origin='lower')
ax.set_xlabel("Amplitude Index")
ax.set_ylabel("Frequency Index")
ax.set_title("Voltage vs Frequency and Amplitude")
plt.colorbar(im, ax=ax, label="Voltage (V)")
run.log_matplotlib("voltage_heatmap", fig)
plt.close(fig)

signal_gen.close()
oscilloscope.close()

Common SCPI Commands

Power Meter

# Keysight/Agilent Power Meter
instrument.write('*RST')  # Reset
instrument.write(':WAV 1550')  # Set wavelength (nm)
instrument.write(':UNIT DBM')  # Set unit to dBm
power = float(instrument.query(':MEAS:POW?'))  # Measure power

Signal Generator

# Keysight Signal Generator
instrument.write('*RST')
instrument.write(':FREQ 1.55E9')  # Set frequency (Hz)
instrument.write(':POW -10')  # Set power (dBm)
instrument.write(':OUTP ON')  # Enable output
instrument.write(':OUTP OFF')  # Disable output

Oscilloscope

# Tektronix Oscilloscope
instrument.write('*RST')
instrument.write(':ACQ:TYPE AVER')  # Set acquisition type
instrument.write(':ACQ:AVER 100')  # Set averaging
voltage = float(instrument.query(':MEAS:VOLT:MAX?'))  # Measure max voltage

Network Analyzer

# Keysight Network Analyzer
instrument.write('*RST')
instrument.write(':SENS:FREQ:STAR 1E9')  # Start frequency
instrument.write(':SENS:FREQ:STOP 10E9')  # Stop frequency
instrument.write(':SENS:SWE:POIN 201')  # Number of points
instrument.write(':INIT')  # Start sweep
instrument.query('*OPC?')  # Wait for completion
s21 = instrument.query(':CALC:DATA:SNP?')  # Get S21 data

Best Practices

Error Handling

import os
import pyvisa
from optixlog import Optixlog

client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="HardwareTesting", create_if_not_exists=True)
run = project.run(name="hardware_test", config={})

rm = pyvisa.ResourceManager()
instrument = None

try:
    instrument = rm.open_resource('USB0::0x1234::0x5678::INSTR')
    
    # Test code here
    power = float(instrument.query(':MEAS:POW?'))
    run.log(step=0, power=power)
        
except pyvisa.errors.VisaIOError as e:
    print(f"Instrument error: {e}")
    run.log(step=0, error=str(e), status="failed")
except Exception as e:
    print(f"Unexpected error: {e}")
    run.log(step=0, error=str(e), status="failed")
finally:
    if instrument:
        instrument.close()

Timeout Configuration

instrument = rm.open_resource('USB0::0x1234::0x5678::INSTR')
instrument.timeout = 10000  # 10 seconds timeout

Instrument Identification

import os
from optixlog import Optixlog

# Query instrument identification
idn = instrument.query('*IDN?')
print(f"Instrument: {idn}")

# Log instrument info in config
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="HardwareTesting", create_if_not_exists=True)
run = project.run(
    name="test",
    config={"instrument": idn.strip()}
)

Real-World Example: Optical Component Testing

import os
import time
import numpy as np
import matplotlib.pyplot as plt
import pyvisa
from optixlog import Optixlog

# Initialize OptixLog
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="OpticalComponentTesting", create_if_not_exists=True)

# Connect to instruments
rm = pyvisa.ResourceManager()
laser = rm.open_resource('USB0::0x1234::0x5678::INSTR')
power_meter = rm.open_resource('USB0::0xABCD::0xEF01::INSTR')

# Test configuration
wavelengths = np.linspace(1520, 1580, 200)
input_power = -5  # dBm

run = project.run(
    name="optical_component_test",
    config={
        "component": "Waveguide Coupler",
        "wavelength_range": [1520, 1580],
        "input_power": input_power,
        "test_date": "2025-11-24"
    }
)

# Initialize instruments
laser.write('*RST')
power_meter.write('*RST')
power_meter.write(':WAV 1550')

transmission = []
insertion_loss = []

for i, wavelength in enumerate(wavelengths):
    # Set laser wavelength and power
    laser.write(f':WAV {wavelength}')
    laser.write(f':POW {input_power}')
    laser.write(':OUTP ON')
    time.sleep(0.2)  # Stabilization time
    
    # Measure output power
    output_power = float(power_meter.query(':MEAS:POW?'))
    transmission.append(output_power)
    insertion_loss.append(input_power - output_power)
    
    # Log measurement
    run.log(
        step=i,
        wavelength=wavelength,
        input_power=input_power,
        output_power=output_power,
        insertion_loss=input_power - output_power
    )

# Create and log transmission spectrum
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(wavelengths, transmission, 'b-', linewidth=1)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Power (dBm)")
ax.set_title("Transmission Spectrum")
ax.grid(True, alpha=0.3)
run.log_matplotlib("transmission_spectrum", fig)
plt.close(fig)

# Create and log insertion loss plot
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(wavelengths, insertion_loss, 'r-', linewidth=1)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Loss (dB)")
ax.set_title("Insertion Loss")
ax.grid(True, alpha=0.3)
run.log_matplotlib("insertion_loss", fig)
plt.close(fig)

# Log final statistics
run.log(
    step=len(wavelengths),
    min_loss=np.min(insertion_loss),
    max_loss=np.max(insertion_loss),
    avg_loss=np.mean(insertion_loss)
)

laser.close()
power_meter.close()

Integration with Test Automation

PyTest Integration

import os
import pytest
import pyvisa
from optixlog import Optixlog

# Initialize OptixLog once for all tests
client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
project = client.project(name="PyTestHardware", create_if_not_exists=True)

@pytest.fixture
def instrument():
    rm = pyvisa.ResourceManager()
    inst = rm.open_resource('USB0::0x1234::0x5678::INSTR')
    yield inst
    inst.close()

def test_power_measurement(instrument):
    run = project.run(
        name="pytest_power_test",
        config={"test": "power_measurement"}
    )
    
    instrument.write('*RST')
    power = float(instrument.query(':MEAS:POW?'))
    
    # Log result
    run.log(step=0, power=power)
    
    # Assert
    assert -10 <= power <= 10, f"Power out of range: {power} dBm"

def test_frequency_response(instrument):
    run = project.run(
        name="pytest_frequency_test",
        config={"test": "frequency_response"}
    )
    
    frequencies = [1e9, 5e9, 10e9]
    
    for i, freq in enumerate(frequencies):
        instrument.write(f':FREQ {freq}')
        response = float(instrument.query(':MEAS:POW?'))
        run.log(step=i, frequency=freq, response=response)
        
        assert response > -30, f"Response too low at {freq} Hz"

Next Steps

On this page