Skip to content

Basic Usage

This guide covers the fundamental concepts and patterns for using SCPI Instrument Control, a universal Python library for SCPI test equipment — oscilloscopes, function generators/AWGs, power supplies, and DAQ units — with a PyQt6 desktop GUI and a browser-based lab gateway.

Connecting to Your Oscilloscope

Basic Connection

The simplest way to connect is by creating an Oscilloscope instance with the IP address:

from scpi_control import Oscilloscope

# Create instance
scope = Oscilloscope('192.168.1.100')

# Connect
scope.connect()

# Get device information
print(scope.identify())

# When done
scope.disconnect()

The recommended approach is using a context manager, which automatically handles connection and disconnection:

from scpi_control import Oscilloscope

with Oscilloscope('192.168.1.100') as scope:
    # Connection happens automatically
    print(scope.identify())
    # Work with oscilloscope...
    # Disconnection happens automatically

!!! tip "Why Use Context Manager?" - Automatic connection/disconnection - Ensures cleanup even if errors occur - More concise and Pythonic - Prevents resource leaks

Connection Parameters

You can customize the connection settings:

scope = Oscilloscope(
    host='192.168.1.100',      # IP address or hostname
    port=5024,                  # TCP port (default: 5024)
    timeout=10.0                # Command timeout in seconds (default: 5.0)
)

Device Information

Getting Identification

# Full identification string
idn = scope.identify()
print(idn)
# Output: Siglent Technologies,SDS824X HD,SDSMMDD1XXXXX,8.2.5.1.37R9

# Parsed device information
info = scope.device_info
print(f"Manufacturer: {info['manufacturer']}")
print(f"Model: {info['model']}")
print(f"Serial: {info['serial']}")
print(f"Firmware: {info['firmware']}")

Model Capabilities

The library automatically detects your oscilloscope model and its capabilities:

caps = scope.model_capability

print(f"Series: {caps.series}")
print(f"Model: {caps.model_name}")
print(f"Channels: {caps.num_channels}")
print(f"Bandwidth: {caps.bandwidth_mhz} MHz")
print(f"Sample Rate: {caps.max_sample_rate/1e9} GSa/s")
print(f"Memory Depth: {caps.max_memory_depth/1e6} Mpts")

!!! info "Supported Models" - SDS800X HD Series (e.g., SDS824X HD) - SDS1000X-E Series - SDS2000X Plus Series - SDS5000X Series

Working with Channels

Accessing Channels

Channels are accessed as properties of the oscilloscope:

# Access specific channels
ch1 = scope.channel1
ch2 = scope.channel2
ch3 = scope.channel3
ch4 = scope.channel4

# Or dynamically
channel_num = 1
ch = getattr(scope, f'channel{channel_num}')

Enabling/Disabling Channels

# Enable channel
scope.channel1.enabled = True
# Or
scope.channel1.enable()

# Disable channel
scope.channel1.enabled = False
# Or
scope.channel1.disable()

# Check if enabled
if scope.channel1.enabled:
    print("Channel 1 is active")

Basic Channel Configuration

# Configure channel 1
scope.channel1.enabled = True
scope.channel1.voltage_scale = 1.0       # 1V per division
scope.channel1.voltage_offset = 0.0      # Center on 0V
scope.channel1.coupling = "DC"           # DC coupling
scope.channel1.probe_ratio = 10.0        # 10X probe

Coupling Modes

# Available coupling modes
scope.channel1.coupling = "DC"    # DC coupling
scope.channel1.coupling = "AC"    # AC coupling (blocks DC)
scope.channel1.coupling = "GND"   # Ground (for offset calibration)

Voltage Scale

# Set voltage scale (volts per division)
scope.channel1.voltage_scale = 1.0    # 1V/div
scope.channel1.voltage_scale = 0.5    # 500mV/div
scope.channel1.voltage_scale = 2.0    # 2V/div

# Common values: 0.001, 0.002, 0.005, 0.01, 0.02, 0.05,
#                0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0

# Read current scale
scale = scope.channel1.voltage_scale
print(f"Current scale: {scale} V/div")

Voltage Offset

# Set vertical offset
scope.channel1.voltage_offset = 0.0     # Center on screen
scope.channel1.voltage_offset = 2.0     # Shift up by 2V
scope.channel1.voltage_offset = -1.5    # Shift down by 1.5V

# Read current offset
offset = scope.channel1.voltage_offset
print(f"Current offset: {offset} V")

Probe Ratio

# Common probe ratios
scope.channel1.probe_ratio = 1.0      # 1X probe (direct)
scope.channel1.probe_ratio = 10.0     # 10X probe (most common)
scope.channel1.probe_ratio = 100.0    # 100X probe (high voltage)

# Read current probe ratio
ratio = scope.channel1.probe_ratio
print(f"Probe ratio: {ratio}X")

Bandwidth Limiting

# Enable 20 MHz bandwidth limit (reduces noise)
scope.channel1.bandwidth_limit = "ON"

# Disable bandwidth limit (full bandwidth)
scope.channel1.bandwidth_limit = "OFF"

# Check current setting
bwl = scope.channel1.bandwidth_limit

Getting Channel Configuration

# Get all channel settings at once
config = scope.channel1.get_configuration()

print(f"Enabled: {config['enabled']}")
print(f"Coupling: {config['coupling']}")
print(f"Scale: {config['voltage_scale']} V/div")
print(f"Offset: {config['voltage_offset']} V")
print(f"Probe: {config['probe_ratio']}X")
print(f"BWL: {config['bandwidth_limit']}")

Trigger Configuration

Basic Trigger Setup

# Configure trigger
scope.trigger.mode = "AUTO"        # Auto, Normal, Single, or Stop
scope.trigger.source = "C1"        # Trigger source (C1, C2, C3, C4, EXT, LINE)
scope.trigger.level = 0.0          # Trigger level in volts
scope.trigger.slope = "POS"        # Rising edge (POS) or falling edge (NEG)

Trigger Modes

# AUTO mode - triggers automatically even without signal
scope.trigger.mode = "AUTO"

# NORMAL mode - only triggers when condition is met
scope.trigger.mode = "NORMAL"

# SINGLE mode - triggers once then stops
scope.trigger.mode = "SINGLE"

# STOP mode - stops triggering
scope.trigger.mode = "STOP"

Trigger Sources

# Trigger on channels
scope.trigger.source = "C1"    # Channel 1
scope.trigger.source = "C2"    # Channel 2
scope.trigger.source = "C3"    # Channel 3
scope.trigger.source = "C4"    # Channel 4

# External trigger
scope.trigger.source = "EXT"   # External trigger input

# Line trigger
scope.trigger.source = "LINE"  # AC line frequency

Trigger Level

# Set trigger level (in volts)
scope.trigger.level = 0.0      # Trigger at 0V
scope.trigger.level = 1.5      # Trigger at 1.5V
scope.trigger.level = -0.5     # Trigger at -0.5V

# Read current level
level = scope.trigger.level
print(f"Trigger level: {level} V")

Acquisition Control

Run/Stop Control

# Start acquisition
scope.run()

# Stop acquisition
scope.stop()

# Check acquisition state
if scope.is_running:
    print("Acquisition running")
else:
    print("Acquisition stopped")

Single Trigger

# Trigger single acquisition (useful in NORMAL mode)
scope.trigger_single()

# Wait for trigger to complete
import time
timeout = 5.0
start = time.time()
while (time.time() - start) < timeout:
    status = scope.query(":TRIG:STAT?").strip()
    if status == "Stop":
        print("Trigger captured!")
        break
    time.sleep(0.1)
else:
    print("Trigger timeout")

Timebase Configuration

# Set timebase (seconds per division)
scope.timebase = 1e-3          # 1 ms/div
scope.timebase = 100e-6        # 100 µs/div
scope.timebase = 1e-6          # 1 µs/div

# Read current timebase
tb = scope.timebase
print(f"Timebase: {tb*1e6} µs/div")

Basic Waveform Acquisition

Single Channel Capture

# Capture waveform from channel 1
waveform = scope.get_waveform(channel=1)

# Access waveform data
print(f"Channel: {waveform.channel}")
print(f"Samples: {len(waveform.voltage)}")
print(f"Sample rate: {waveform.sample_rate/1e9} GSa/s")
print(f"Time range: {waveform.time[0]} to {waveform.time[-1]} s")

# Waveform data as numpy arrays
times = waveform.time          # Time values (seconds)
voltages = waveform.voltage    # Voltage values (volts)

Multi-Channel Capture

# Enable multiple channels
scope.channel1.enabled = True
scope.channel2.enabled = True

# Capture all enabled channels
waveforms = scope.get_waveforms()

# Process each waveform
for wf in waveforms:
    print(f"Channel {wf.channel}: {len(wf.voltage)} samples")

Direct SCPI Commands

For advanced control, you can send raw SCPI commands:

# Write a command
scope.write("*RST")  # Reset oscilloscope

# Query a value
response = scope.query("*IDN?")
print(response)

# Query a numeric value
value = scope.query_float("C1:VDIV?")
print(f"Channel 1 scale: {value} V/div")

Use High-Level API When Possible

The library provides high-level methods for most operations. Only use direct SCPI commands when the functionality isn't available through the API.

Error Handling

Common Exceptions

from scpi_control import (
    SiglentConnectionError,
    SiglentTimeoutError,
    InvalidParameterError,
    CommandError
)

try:
    scope = Oscilloscope('192.168.1.100')
    scope.connect()

    # Operations...

except SiglentConnectionError as e:
    print(f"Connection failed: {e}")
except SiglentTimeoutError as e:
    print(f"Command timeout: {e}")
except InvalidParameterError as e:
    print(f"Invalid parameter: {e}")
except CommandError as e:
    print(f"Command error: {e}")
finally:
    scope.disconnect()
from scpi_control import Oscilloscope

try:
    with Oscilloscope('192.168.1.100') as scope:
        # Your code here
        waveform = scope.get_waveform(1)

except Exception as e:
    print(f"Error: {e}")
    import traceback
    traceback.print_exc()

Complete Example

Here's a complete example putting it all together:

from scpi_control import Oscilloscope

# Configuration
SCOPE_IP = '192.168.1.100'

with Oscilloscope(SCOPE_IP) as scope:
    # Get device info
    print(f"Connected to: {scope.device_info['model']}")

    # Configure channel 1
    scope.channel1.enabled = True
    scope.channel1.voltage_scale = 1.0
    scope.channel1.voltage_offset = 0.0
    scope.channel1.coupling = "DC"
    scope.channel1.probe_ratio = 10.0

    # Configure trigger
    scope.trigger.mode = "AUTO"
    scope.trigger.source = "C1"
    scope.trigger.level = 0.0
    scope.trigger.slope = "POS"

    # Set timebase
    scope.timebase = 1e-3  # 1 ms/div

    # Start acquisition
    scope.run()

    # Capture waveform
    waveform = scope.get_waveform(1)
    print(f"Captured {len(waveform.voltage)} samples")
    print(f"Time range: {waveform.time[0]*1e3:.3f} to {waveform.time[-1]*1e3:.3f} ms")
    print(f"Voltage range: {waveform.voltage.min():.3f} to {waveform.voltage.max():.3f} V")

Best Practices

!!! tip "Connection Management" - Always use context managers (with statement) when possible - Ensure disconnect() is called in finally blocks if not using context manager - Set appropriate timeouts based on your network and operation type

!!! tip "Channel Configuration" - Set probe ratio to match your actual probe (usually 10X) - Use appropriate coupling: DC for DC-coupled signals, AC to remove DC offset - Enable bandwidth limiting to reduce high-frequency noise when not needed

!!! tip "Performance" - Only enable channels you need to capture - Use appropriate timebase and memory depth for your application - Consider using get_waveforms() for multi-channel capture instead of multiple get_waveform() calls

!!! tip "Error Handling" - Always wrap oscilloscope operations in try/except blocks - Handle network timeouts appropriately - Log errors for debugging

Next Steps