From 9451066ed6f6b55c2c50720d0f68ab9d30ed38d6 Mon Sep 17 00:00:00 2001 From: {Johann Heyszl} Date: Mon, 25 Sep 2023 21:46:16 +0200 Subject: [PATCH] Rework WaveRunner to support manual scope config w/o hard-coded defaults Signed-off-by: {Johann Heyszl} --- cw/save_waverunner_config_to_file.py | 5 + cw/test_waverunner.py | 12 +++ cw/waverunner.py | 148 +++++++++++++++++++-------- 3 files changed, 124 insertions(+), 41 deletions(-) mode change 100644 => 100755 cw/save_waverunner_config_to_file.py diff --git a/cw/save_waverunner_config_to_file.py b/cw/save_waverunner_config_to_file.py old mode 100644 new mode 100755 index d0ef8f5d..d52334f8 --- a/cw/save_waverunner_config_to_file.py +++ b/cw/save_waverunner_config_to_file.py @@ -5,6 +5,8 @@ """Save WaveRunner to file for later use by script.""" +from datetime import datetime + from waverunner import WaveRunner if __name__ == '__main__': @@ -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) diff --git a/cw/test_waverunner.py b/cw/test_waverunner.py index d4dfb0ef..e8612501 100755 --- a/cw/test_waverunner.py +++ b/cw/test_waverunner.py @@ -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 @@ -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 diff --git a/cw/waverunner.py b/cw/waverunner.py index 87097ba0..8bb50246 100755 --- a/cw/waverunner.py +++ b/cw/waverunner.py @@ -28,15 +28,13 @@ 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 @@ -44,20 +42,34 @@ class WaveRunner: - 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): @@ -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): @@ -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.", @@ -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 @@ -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) @@ -176,7 +192,8 @@ 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: " @@ -184,6 +201,7 @@ def print_info(manufacturer, model, serial, version, opts): 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'") @@ -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", @@ -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", @@ -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", @@ -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 @@ -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 - len_ = int(data[7:16]) + # Packet format example:b'C1:WF DAT1,#900002002 + 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