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 powerSignal 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 outputOscilloscope
# 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 voltageNetwork 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 dataBest 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 timeoutInstrument 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
- SDK Quick Start — Get started with the SDK
- SDK API Reference — Complete API documentation
- Advanced Usage — Advanced patterns and techniques