Skip to content

Commit

Permalink
Rework WaveRunner to support manual scope config w/o hard-coded defaults
Browse files Browse the repository at this point in the history
Signed-off-by: {Johann Heyszl} <[email protected]>
  • Loading branch information
johannheyszl committed Sep 26, 2023
1 parent f952a77 commit 9451066
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 41 deletions.
5 changes: 5 additions & 0 deletions cw/save_waverunner_config_to_file.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

"""Save WaveRunner to file for later use by script."""

from datetime import datetime

from waverunner import WaveRunner

if __name__ == '__main__':
Expand All @@ -15,3 +17,6 @@
now_str = now.strftime("%Y-%m-%d_%H:%M:%S")
file_name_local = f"scope_config_{now_str}.lss"
waverunner.save_setup_to_local_file(file_name_local)

file_name_local = "scope_config.lss"
waverunner.save_setup_to_local_file(file_name_local)
12 changes: 12 additions & 0 deletions cw/test_waverunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from waverunner import WaveRunner

if __name__ == '__main__':
# Create WaveRunner
waverunner = WaveRunner("172.26.111.125")

# Save WaveRunner setup to timestamped file
Expand All @@ -23,3 +24,14 @@
waverunner.save_setup_to_local_file(file_name_local)
# Load setup from local file
waverunner.load_setup_from_local_file(file_name_local)

# Configuration: Choose num_samples and first_point
# num_segments, sparsing, num_samples, first_point, acqu_channel
waverunner.configure_waveform_transfer_general(5, 1, 1000, 0, "C1")

# Loop
waverunner.arm()
waves = waverunner.capture_and_transfer_waves()

# plot waves
# TODO
148 changes: 107 additions & 41 deletions cw/waverunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,48 @@ def __exit__(self, exc_type, exc_val, exc_tb):
class WaveRunner:
"""Class for capturing traces using a WaveRunner oscilloscope.
This class operates the oscilloscope in sequence mode to improve performance and in
2-channel mode to maximize sampling rate. Trigger and power signals must be
connected to channels 2 and 3, respectively.
This class can be used to operate the oscilloscope in sequence mode. The
configuration can be done manually and loaded from a file using provided
functions. In sequence mode, the oscilloscope captures a total of
`num_segments` waves each starting at a trigger event. This is much more
efficient than sending a separate command for each wave.
When in sequence mode, the oscilloscope captures a total of `num_segments` waves
each starting at a trigger event. This is much more efficient than sending a
separate command for each wave.
This class is only tested to work with WaveRunner 9104 series.
This class is only tested with WaveRunner 9104 series.
For more information on the commands used in this module please see:
- Operator's Manual WaveRunner 9000 and WaveRunner 8000-R Oscilloscopes
(http://cdn.teledynelecroy.com/files/manuals/waverunner-9000-operators-manual.pdf)
- MAUI Oscilloscopes Remote Control and Automation Manual
(http://cdn.teledynelecroy.com/files/manuals/maui-remote-control-and-automation-manual.pdf)
Typical usage:
Typical usage with default configuration:
>>> waverunner = WaveRunner('192.168.1.227')
>>> waverunner.load_setup_from_local_file(file_name_local)
>>> waverunner.configure_waveform_transfer_general(num_segments=10, sparsing=1, \\
num_samples=1000, first_point=0)
>>> while foo:
>>> ...
>>> waverunner.arm()
>>> waves = waverunner.capture_and_transfer_waves()
>>> ...
The class also provides a default configuration through functions. Then,
the trigger and power signals must be connected to channels 2 and 3. Note
that the configuration is through hard-coded parameters in this file!
Typical usage with default configuration:
>>> waverunner = WaveRunner('192.168.1.227')
>>> waverunner.configure()
>>> while foo:
>>> ...
>>> waverunner.num_segments = num_segments
>>> waverunner.arm()
>>> waves = waverunner.wait_for_acquisition_and_transfer_waves()
>>> waves = waverunner.capture_and_transfer_waves()
>>> ...
Attributes:
num_segments_max: Maximum number of waves per sequence.
num_segments: Number of waves per sequence.
num_segments_actual: Equal to ``num_segments``.
num_segments: Number of segments per sequence.
num_samples: Number of samples per segment.
"""

def __init__(self, ip_addr):
Expand All @@ -70,10 +82,11 @@ def __init__(self, ip_addr):
"""
self._ip_addr = ip_addr
self.num_segments = 1000
self._num_samples = 740
self.num_samples = 740
self._instr = vxi11.Instrument(self._ip_addr)
self._populate_device_info()
self._print_device_info()
self.acqu_channel = "C3"

@property
def num_segments_max(self):
Expand All @@ -96,7 +109,8 @@ def _get_and_print_cmd_error(self):
"""Get command error status for last command. On error, displays error message."""
# from p.328
# https://cdn.teledynelecroy.com/files/manuals/maui-remote-control-and-automation-manual.pdf
# Note: beware of numbering; idx starts at 0
# Note: Scope error log under Utilities/Remote/Show Remote Control Log
# Note: Beware of numbering; idx starts at 0
error_msg = ["OK.",
"Unrecognized command/query header.",
"Illegal header path.",
Expand Down Expand Up @@ -143,6 +157,7 @@ def _recall_setup_from_file_on_scope(self, file_name_scope):
self._get_and_print_cmd_error()

def save_setup_to_local_file(self, file_name_local):
print("WAVERUNNER: Saving setup to " + file_name_local)
setup_data = self._ask("PANEL_SETUP?")
self._get_and_print_cmd_error()
# remove echoed command characters from beginning
Expand All @@ -154,7 +169,8 @@ def save_setup_to_local_file(self, file_name_local):
def load_setup_from_local_file(self, file_name_local):
# Note: Preserve line endings so that lenght matches
# File probably received/stored from Windows scope with \r\n
local_file = open(file_name_local, "r", newline='')
print("WAVERUNNER: Loading setup from " + file_name_local)
local_file = open(file_name_local, "r", newline='', encoding='utf-8')
data_read_from_file = local_file.read()
file_name_scope = "'D:\\Temporary_setup.lss'"
self._write_to_file_on_scope(file_name_scope, data_read_from_file)
Expand All @@ -176,14 +192,16 @@ def _populate_device_info(self):
def _print_device_info(self):
def print_info(manufacturer, model, serial, version, opts):
# TODO: logging
print(f"Connected to {manufacturer} {model} (ip: {self._ip_addr}, serial: {serial}, "
print(f"WAVERUNNER: Connected to {manufacturer} {model} (ip: "
f"{self._ip_addr}, serial: {serial}, "
f"version: {version}, options: {opts})")
if opts == "WARNING : CURRENT REMOTE CONTROL INTERFACE IS TCPIP":
print("ERROR: WAVERUNNER: Must set remote control to VXI11 on scope under: "
"Utilities > Utilities Setup > Remote")
print_info(**self._device_info)

def _default_setup(self):
# Note this is a default configuration and might not be meaningfull always
self._instr.timeout = 10
# Reset the app and wait until it's done.
self._write("vbs 'app.settodefaultsetup'")
Expand All @@ -196,7 +214,7 @@ def _default_setup(self):
# Hide all traces for better performance.
"C1:TRA OFF",
"C2:TRA OFF",
"C3:TRA OFF",
f"{self.acqu_channel}:TRA OFF",
"C4:TRA OFF",
# Single grid.
"GRID SINGLE",
Expand All @@ -206,21 +224,23 @@ def _default_setup(self):
"*OPC?",
]
res = self._ask(";".join(commands))
assert res == "1"
assert res == "*OPC 1"

def _configure_power_channel(self):
# Note this is a default configuration and might not be meaningfull always
commands = [
# DC coupling, 1 Mohm.
"C3:CPL D1M",
"C3:VDIV 35MV",
"C3:OFST 105MV",
f"{self.acqu_channel}:CPL D1M",
f"{self.acqu_channel}:VDIV 35MV",
f"{self.acqu_channel}:OFST 105MV",
]
self._write(";".join(commands))
self._write("vbs 'app.Acquisition.C3.BandwidthLimit = \"200MHz\"'")
self._write(f"vbs 'app.Acquisition.{self.acqu_channel}.BandwidthLimit = \"200MHz\"'")
# Noise filtering - reduces bandwidth.
self._write("vbs 'app.Acquisition.C3.EnhanceResType = \"2.5bits\"'")
self._write(f"vbs 'app.Acquisition.{self.acqu_channel}.EnhanceResType = \"2.5bits\"'")

def _configure_trigger_channel(self):
# Note this is a default configuration and might not be meaningfull always
commands = [
# DC coupling, 1 Mohm.
"C2:CPL D1M",
Expand All @@ -232,6 +252,7 @@ def _configure_trigger_channel(self):
self._write("vbs 'app.Acquisition.C2.BandwidthLimit = \"200MHz\"'")

def _configure_trigger(self):
# Note this is a default configuration and might not be meaningfull always
commands = [
# Select trigger: edge, channel 2, no hold-off.
"TRSE EDGE,SR,C2,HT,OFF",
Expand All @@ -245,6 +266,7 @@ def _configure_trigger(self):
self._write(";".join(commands))

def _configure_timebase(self):
# Note this is a default configuration and might not be meaningfull always
commands = [
"TDIV 800NS",
# Trigger delay: Trigger is centered by default. Move to the left to
Expand All @@ -255,67 +277,111 @@ def _configure_timebase(self):
]
self._write(";".join(commands))

def _configure_acquisition(self):
def _configure_acqu(self):
# Note this is a default configuration and might not be meaningfull always
# Only use channels 2 and 3 to maximize sampling rate.
self._write("vbs 'app.Acquisition.Horizontal.ActiveChannels = \"2\"'")
self._write("vbs 'app.Acquisition.Horizontal.Maximize = \"FixedSampleRate\"'")
self._write("vbs 'app.Acquisition.Horizontal.SampleRate = \"1 GS/s\"'")

def _configure_waveform_transfer(self):
# Note this is a default configuration and might not be meaningfull always
commands = [
# SP: decimation, 10 for 1 GS/s -> 100 MS/s.
# NP: number of points, self._num_samples.
# NP: number of points, self.num_samples.
# FP: first point (without decimation).
# SN: All sequences: 0
f"WFSU SP,10,NP,{self._num_samples},FP,10,SN,0",
f"WFSU SP,10,NP,{self.num_samples},FP,10,SN,0",
# Data format: with DEF9 header, bytes (8-bit signed integers), binary encoding.
# TODO: byte vs. word.
"CFMT DEF9,BYTE,BIN",
# LSB first.
"CORD LO",
]
self._write(";".join(commands))

def _configure(self):
"""Configures the oscilloscope for acquisition."""
def configure_waveform_transfer_general(self,
num_segments,
sparsing,
num_samples,
first_point,
acqu_channel):
"""Configures the oscilloscope for acqu with given parameters."""
print(f"WAVERUNNER: Configuring with num_segments={num_segments}, "
f"sparsing={sparsing}, num_samples={num_samples}, "
f"first_point={first_point}, acqu_channel=" + acqu_channel)
self.num_samples = num_samples
self.num_segments = num_segments
self.acqu_channel = acqu_channel
commands = [
# WAVEFORM_SETUP
# SP: sparsing, e.g. 10 for every 10th point, 1 for every point.
# NP: number of points, self.num_samples.
# FP: first point (without sparsing).
# SN: All sequences shall be sent: 0.
f"WFSU SP,{sparsing},NP,{num_samples},FP,{first_point},SN,0",
# COMM_FORMAT
# Data format: with DEF9 header, bytes (8-bit signed integers), binary encoding.
# TODO: We currently transfer bytes. Use WORD for larger ADCs
"CFMT DEF9,BYTE,BIN",
# COMM_ORDER
# LO means LSB first.
"CORD LO",
]
self._write(";".join(commands))
self._get_and_print_cmd_error()

def configure(self):
"""Configures the oscilloscope for acqu with default configuration."""
# Note this is a default configuration and might not be meaningfull always
self._default_setup()
self._configure_power_channel()
self._configure_trigger_channel()
self._configure_trigger()
self._configure_timebase()
self._configure_acquisition()
self._configure_acqu()
self._configure_waveform_transfer()

def arm(self):
"""Arms the oscilloscope in sequence mode."""
"""Arms the oscilloscope in sequence mode for selected channel."""
# SEQ SEQUENCE Mode
# TRMD Trigger Mode Single
commands = [
f"SEQ ON,{self.num_segments}",
"TRMD SINGLE",
"*OPC?",
]
res = self._ask(";".join(commands))
assert res == "1"
assert res == "*OPC 1"

def _parse_waveform(self, data):
# Packet format: DAT1,#9000002002<SAMPLES>
len_ = int(data[7:16])
# Packet format example:b'C1:WF DAT1,#900002002<SAMPLES>
len_ = int(data[13:22])
# Note: We use frombufer to minimize processing overhead.
waves = np.frombuffer(data, np.int8, int(len_), 16)
waves = waves.reshape((self.num_segments, self._num_samples))
waves = np.frombuffer(data, np.int8, int(len_), 22)
waves = waves.reshape((self.num_segments, self.num_samples))
return waves

def capture_and_transfer_waves(self):
"""Waits until the acquisition is complete and transfers waveforms.
"""Waits until the acqu is complete and transfers waveforms.
Returns:
Waveforms.
"""
# Don't process commands until the acquisition is complete and wait until
# Don't process commands until the acqu is complete and wait until
# processing is complete.
res = self._ask("WAIT 10;*OPC?")
assert res == "1"
assert res == "*OPC 1"
# Transfer and parse waveform data.
data = self._ask_raw(b"C3:WF? DAT1")
if self.acqu_channel == "C1":
data = self._ask_raw(b"C1:WF? DAT1")
elif self.acqu_channel == "C2":
data = self._ask_raw(b"C2:WF? DAT1")
elif self.acqu_channel == "C3":
data = self._ask_raw(b"C3:WF? DAT1")
elif self.acqu_channel == "C4":
data = self._ask_raw(b"C4:WF? DAT1")
else:
print("WAVERUNNER: Error: Channel selection invalid")
waves = self._parse_waveform(data)
return waves

Expand Down

0 comments on commit 9451066

Please sign in to comment.