From 2338fe608abfc2a04c3ef158098357177b98d418 Mon Sep 17 00:00:00 2001 From: Code-Craftsman-Christian Date: Wed, 7 Aug 2024 11:29:16 +0200 Subject: [PATCH 1/9] main.py for Keysight EXR/MXR/UXR Oscilloscopes This driver was tested on a Keysight EXR scope, but MXR and UXR should work fine as well as only basic functionality was implemented and all three scopes run on the same software. Development of the driver began with the Tek7k driver as a starting point but just few pieces are left. Beside configuration of channels, trigger and sample rate, the sample rate type option was misused to implement the possibility of ERES, offering the tradin of bandwidth and sample rate for an increase in ENOB, up to 16bit. Some wait times had to be inserted to take care of processing time on the scope. Especially if you start the scope with default settings and request it to change pretty much everything from vertical scale to horizontal scale, trigger type, trigger edge, etc. etc. it could be that it triggers before finalizing on all changes, throwing an "invalid waveform" error because the captured one in memory does not fit the requested parameters. The data transfer uses ASCii as it is nice to handle during debugging and requires not processing after retrival. If speed is a concern, a switch to WORD format including processing of the values needs to be implemented. --- src/Keysight_EXR/main.py | 335 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/Keysight_EXR/main.py diff --git a/src/Keysight_EXR/main.py b/src/Keysight_EXR/main.py new file mode 100644 index 00000000..9f212a03 --- /dev/null +++ b/src/Keysight_EXR/main.py @@ -0,0 +1,335 @@ +# This Device Class is published under the terms of the MIT License. +# Required Third Party Libraries, which are included in the Device Class +# package for convenience purposes, may have a different license. You can +# find those in the corresponding folders or contact the maintainer. + +# MIT License + +# Copyright (c) 2024 SweepMe! GmbH + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Contribution: We like to thank Peter Hegarty (TU Dresden) for providing the initial version of this driver. + +# SweepMe! device class +# Type: Scope +# Device: Keysight EXR/MXR/UXR Series (tested on EXR only) + + +from EmptyDeviceClass import EmptyDevice +import numpy as np +import time as time + +class Device(EmptyDevice): + + description = """ + Main functions only. + """ + + def __init__(self): + + EmptyDevice.__init__(self) + + self.shortname = "EXRxxxA" + + self.variables = ["Time"] + self.units = ["s"] + self.plottype = [True] # True to plot data + self.savetype = [True] # True to save data + + self.port_manager = True + self.port_types = ["USB"] + self.port_identifications = ['Keysight,EXR*'] + + self.port_properties = { + "timeout": 10.0, + "delay": 1.0, + } + + + self.commands = { + "Channel 1": "CH1", + "Channel 2": "CH2", + "Channel 3": "CH3", + "Channel 4": "CH4", + "External": "EXT", + "Line": "LINE", + "None": "NONE", + "Rising": "POS", + "Falling": "NEG", + } + + def set_GUIparameter(self): + + GUIparameter = { + "SweepMode": ["None"], + + "TriggerSlope": ["As is", "Rising", "Falling"], + "TriggerSource": ["As is", "CHAN1", "CHAN2", "CHAN3", "CHAN4", "AUX", "LINE"], + # "TriggerCoupling": ["As is", "AC", "DC", "HF", "Auto level"], # not yet implemented + "TriggerLevel": 0, + "TriggerDelay": 0, + "TriggerTimeout": 2, + "TimeRange": ["Time range in s", "Time scale in s/div"], + "TimeRangeValue": 5e-4, + "TimeOffsetValue": 0.0, + "SamplingRate": ["10e+3", "100e+3", "1e+6", "10e+6", "100e+6", "1e+9","5e+9","10e+9","16e+9"], + "SamplingRateType":["BITS10: 10 Bits (16 GSa or Manual)","BITS11: 11 Bits (6.4 GSa)","BITS12: 12 Bits (3.2 GSa)","BITS13: 13 Bits (1.6 GSa)","BITS14: 14 Bits (800 MSa)","BITS15: 15 Bits (400 GSa)","BITS16: 16 Bits (200 GSa)","BITS16_4: 16 Bits (100 GSa)","BITS16_2: 16 Bits (50 GSa)"], + "Acquisition": ["Continuous", "Single"], + "Average": ["none", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"], + "VoltageRange": ["Voltage range in V"], + } + + for i in range(1,5): + GUIparameter["Channel%i" % i] = True if i == 1 else False + GUIparameter["Channel%i_Name" % i] = "CH%i" % i + GUIparameter["Channel%i_Range" % i] = ["4e-2", "8e-2", "2e-1", "4e-1", "8e-1", "2", "4", "8", "20", "40", "80", "200"] + GUIparameter["Channel%i_Offset" % i] = 0.0 + + return GUIparameter + + + def get_GUIparameter(self, parameter={}): + #print(parameter) + + self.triggersource = parameter["TriggerSource"] + self.triggercoupling = parameter["TriggerCoupling"] + self.triggerslope = parameter["TriggerSlope"] + self.triggerlevel = parameter["TriggerLevel"] + self.triggerdelay = parameter["TriggerDelay"] + self.triggertimeout = parameter["TriggerTimeout"] + + self.timerange = parameter["TimeRange"] + self.timerangevalue = float(parameter["TimeRangeValue"]) + self.timeoffsetvalue = parameter["TimeOffsetValue"] + self.samplingrate = parameter["SamplingRate"] + self.samplingratetype = parameter["SamplingRateType"] #reads the chosen "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + + if self.samplingratetype.startswith("BITS16_"): #chosen ADC resolution is taken from the GUI selection and modified so it can be used as the required parameter in the respective SCPI command + self.samplingratetype = self.samplingratetype[:8] + else: + self.samplingratetype = self.samplingratetype[:6] + + self.acquisition = parameter["Acquisition"] + self.average = parameter["Average"] + + self.channels = [] + self.channel_names = {} + self.channel_ranges = {} + self.channel_divs = {} + self.channel_offsets = {} + + for i in range(1,5): + + if parameter["Channel%i" % i]: + self.channels.append(i) + + self.variables.append(self.commands["Channel %i" % i] + " " + parameter["Channel%i_Name" % i]) + self.units.append("V") + self.plottype.append(True) + self.savetype.append(True) + self.channel_names[i] = parameter["Channel%i_Name" % i] + self.channel_ranges[i] = float(parameter["Channel%i_Range" % i]) + self.channel_divs[i] = self.channel_ranges[i] / 8 + self.channel_offsets[i] = parameter["Channel%i_Offset" % i] + + + def initialize(self): + # This driver does not use Reset yet so that user can do measurements with changing options manually + #self.port.write("*RST") + + if len(self.channels) == 0: + raise Exception("Please select at least one channel to be read out") + + if int(10*float(self.triggertimeout)) % 2 != 0: + msg = "Trigger timeout can only be set in steps of 0.2s" + raise Exception(msg) + #values are multiplied by 10 to allow comparison operation in integer realm + + #self.port.write("*IDN?") # Query device name + #print("ID Checkup") + #print(self.port.read()) + + self.port.write("*CLS") # Clears all the event registers, and also clears the error queue. + + self.port.write(":WAV:FORM ASC") # sets encoding to ASCii insetad of BYTE or WORD; BYTE and WORD would allow for a much quicker data transfer, but values would have to be processed first before use. + self.port.write(":WAV:TYPE RAW") # sets data type to RAW, transmitting only true sampling points, no interpolation + + def configure(self): + + + ### Acquisition ### + self.port.write(":ACQ:MODE RTIM") #use real time aquisition + + if self.average =="none": + self.port.write(":ACQ:AVER OFF") #diable averaging of triggered shots + else: + self.port.write(":ACQ:AVER ON") #enabling averaging of triggered shots + self.port.write(":ACQ:AVER:COUN %s" %self.average) #setting the amount of triggered shots to be used for averaging + + self.port.write(":ACQ:POIN:ANAL AUTO") #set the Memory Depth to AUTO to give priority to sampling rate, mnanual p. 326 + if self.samplingratetype.startswith("BITS10"): + self.port.write(":ACQ:SRAT:ANAL %s" %self.samplingrate) #sets the sampling rate, memory auto-adjusts accordingly. Only allowed with 10bits ADC setting!!! + else: + self.port.write(":ACQ:SRAT:ANAL AUTO") #sampling rate to AUTO for all ADC interpolation options + self.port.write(":ACQ:ADCR %s" %self.samplingratetype) #sets the "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + + + self.port.write(":TIM:REF CENTER") #sets the timebase reference to center; might be a future GUI option to allow for left or right border choice? + + ##self.port.write(":ACQ:AVER %s" %self.average) # set averages + + + ### Trigger ### + # setting trigger source and level + if self.triggersource == "As is": + pass + else: + self.port.write(":TRIG:EDGE:SOUR %s" % self.triggersource) # set trigger source + self.port.write(":TRIG:LEV %s,%s" % (self.triggersource, self.triggerlevel)) # set trigger level + + if self.triggerlevel == 0: # if no specific trigger level desired, + self.port.write(":TRIG:LEV:FIFT") # sets the trigger level at 50% + + if self.triggerslope == "As is": # set trigger slope + pass + elif self.triggerslope== "Rising": + self.port.write(":TRIG:EDGE:SLOP POS") + elif self.triggerslope == "Falling": + self.port.write(":TRIG:EDGE:SLOP NEG") + + self.port.write(":TRIG:DEL:TDEL:TIME %s" % self.triggerdelay) # sets trigger delay; very usefull if scope is combined in a sweepme sequence with other instruments + self.port.write(":TRIG:SWE TRIG") #set trigger sweep mode to TRIGGERED + + ### Time range ### + + if self.timerange == "Time range in s": + self.port.write(":TIM:RANG %s" %self.timerangevalue) #set timebase range + elif self.timerange == "Time scale in s/div": + self.port.write(":TIM:SCAL %s" %self.timerangevalue) #set timebase scale + + if self.timeoffsetvalue == "As is": + pass + else: + self.port.write(":TIM:POS %s" %self.timeoffsetvalue) #set timebase offset + + for i in range(1,5): #makes sure that only activated channels are displayed + if i in self.channels: + self.port.write(":CHAN%s:DISP ON" % i) # turn on selected channels + self.port.write(":CHAN%s:SCAL %s" % (i, self.channel_divs[i])) # scale of channel + self.port.write(":CHAN%s:OFFS %s" % (i, self.channel_offsets[i])) # define offset of channel + else: + self.port.write(":CHAN%s:DISP OFF" % i) # turn off unselected channels + + def apply(self): + pass + + def measure(self): + + if self.average != "none" and self.acquisition.startswith("Cont"): + self.port.write(":CDIS") # clear display to reset the averaging counter when using continous trigger + time.sleep(0.3) + + if self.acquisition.startswith("Single"): + self.port.write(":SING") #performs single acquisition + elif self.acquisition.startswith("Cont"): + self.port.write(":RUN") # run continuous aquisition; not required when using single trigger + + time.sleep(float(self.triggerdelay)) + trigcounter = 0 + while True: + self.port.write(":ADER?") #check if trigger aquisition was successfull; if averaging is enabled, it will return 1 only when all average samples have been taken + triggerstat=int(self.port.read()) + print("triggerstatus:", triggerstat) + if triggerstat == 0 and trigcounter < int(round(float(self.triggertimeout),1)*5): #if no trigger was aquired, wait loops in 0.2s increments + trigcounter += 1 + time.sleep(0.2) + elif trigcounter >= int(round(float(self.triggertimeout),1)*5): #timeout in trigger wait loop, stops the aqusition + self.port.write(":STOP") + if self.average == "none": + msg = "Oscilloscope could not trigger before timeout" + raise Exception(msg) + else: + msg = "Oscilloscope could not trigger for sufficient averaging samples before timeout" + raise Exception(msg) + else: + break + + if self.acquisition.startswith("Cont"): + self.port.write(":STOP") # stop continuous aquisition; not required when using single trigger + + slot = 0 # run variable for data sorting + + + time.sleep(0.2) + self.port.write(":WAV:PRE?") # retrieving the waveform preamble + time.sleep(0.2) + + #This section retrieves the preamble which describes all properties of the stroes waveform. While not all attributes are used, they remain included for debugging purposes. + preamble = self.port.read().split(",") + wav_format=preamble[0] + acq_mode=preamble[1] + numberpoints=int(preamble[2]) + av_count=int(preamble[3]) + x_inc=float(preamble[4]) + x_orig=float(preamble[5]) + x_ref=float(preamble[6]) + y_inc=float(preamble[7]) + y_orig=float(preamble[8]) + y_ref=float(preamble[9]) + #print ("wav_format:", wav_format, "acq_mode:", acq_mode, "numberpoints:", numberpoints, "av_count:", av_count, "x_inc:", x_inc, "x_org:", x_orig, "x_ref:", x_ref, "y_inc:", y_inc, "y_org:", y_orig, "y_ref:", y_ref) + + + for i in self.channels: + + self.port.write(":WAV:SOUR CHAN%s" % i) # select channel to be read + + if slot== 0: # only for first measurement + channels = len(self.channels) # number of measured channels + self.voltages = np.zeros((numberpoints, len(self.channels))) # generate empty array of correct size for channels + data + + self.timecode = np.linspace(x_orig, (x_orig+x_inc*numberpoints), numberpoints) # generate linear time array FROM, TO, STEPSAMOUNT + + self.port.write(":WAV:DATA?;*OPC") # retrieve waveform values from scope + time.sleep(0.2) # give scope time to prepare waveform data for download + datapoints = self.port.read().split(",") # read values from scope + + opccounter = 0 # set counter for OPC loop back to zero + + while True: + self.port.write("*OPC?") # query scope in loop whether [OP]eration of waveform data tranmission is [C]ompleted + completed = int(self.port.read()) + if completed == 0 and opccounter < 25: # loop time hardcoded to 5s + opccounter += 1 + time.sleep(0.2) + else: + break + + data = [] + for i in np.arange(numberpoints): + data.append(datapoints[i]) # put waveform values of current channel into data output list + + data = np.array(data) # convert list to data array + + self.voltages[:, slot] = data # inputs voltage data for channel i into correct column of data array + slot += 1 # set correct column for next channel + + + def call(self): + return [self.timecode] + [self.voltages[:,i] for i in range(self.voltages.shape[1])] From ae5cbd3f04a71105237ffd9340879b4017fb5279 Mon Sep 17 00:00:00 2001 From: Code-Craftsman-Christian Date: Wed, 7 Aug 2024 11:30:27 +0200 Subject: [PATCH 2/9] Add files via upload --- src/Keysight_EXR/license.txt | 28 ++++++++++++++++++++++++++++ src/Keysight_EXR/run.ini | 6 ++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Keysight_EXR/license.txt create mode 100644 src/Keysight_EXR/run.ini diff --git a/src/Keysight_EXR/license.txt b/src/Keysight_EXR/license.txt new file mode 100644 index 00000000..422da62d --- /dev/null +++ b/src/Keysight_EXR/license.txt @@ -0,0 +1,28 @@ +This Device Class is published under the terms of the MIT License. +Required Third Party Libraries, which are included in the Device Class +package for convenience purposes, may have a different license. You can +find those in the corresponding folders or contact the maintainer. + +MIT License + +Copyright (c) 2021 SweepMe! GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Contribution: We like to thank Peter Hegarty (TU Dresden) for providing the initial version of this driver. \ No newline at end of file diff --git a/src/Keysight_EXR/run.ini b/src/Keysight_EXR/run.ini new file mode 100644 index 00000000..a1ece74a --- /dev/null +++ b/src/Keysight_EXR/run.ini @@ -0,0 +1,6 @@ +[run] +version = 1.5.6.18 +started = 1.5.6.18 +tested = failed +hash = 2ed94d021b624737d51d1aec95bdb584 + From 9ac9a6e1ed44c8989f6ed8e865fd09766aa627b6 Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Tue, 20 Aug 2024 16:01:40 +0200 Subject: [PATCH 3/9] License changed, Formatting changes, Contribution statement removed, run.ini removed --- src/Keysight_EXR/license.txt | 6 +- src/Keysight_EXR/main.py | 201 +++++++++++++++++------------------ src/Keysight_EXR/run.ini | 6 -- 3 files changed, 98 insertions(+), 115 deletions(-) delete mode 100644 src/Keysight_EXR/run.ini diff --git a/src/Keysight_EXR/license.txt b/src/Keysight_EXR/license.txt index 422da62d..a3d2d212 100644 --- a/src/Keysight_EXR/license.txt +++ b/src/Keysight_EXR/license.txt @@ -5,7 +5,7 @@ find those in the corresponding folders or contact the maintainer. MIT License -Copyright (c) 2021 SweepMe! GmbH +Copyright (c) 2024 SweepMe! GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -Contribution: We like to thank Peter Hegarty (TU Dresden) for providing the initial version of this driver. \ No newline at end of file +SOFTWARE. \ No newline at end of file diff --git a/src/Keysight_EXR/main.py b/src/Keysight_EXR/main.py index 9f212a03..a22643c8 100644 --- a/src/Keysight_EXR/main.py +++ b/src/Keysight_EXR/main.py @@ -5,7 +5,7 @@ # MIT License -# Copyright (c) 2024 SweepMe! GmbH +# Copyright (c) 2024 SweepMe! GmbH (sweep-me.net) # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,17 +25,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -# Contribution: We like to thank Peter Hegarty (TU Dresden) for providing the initial version of this driver. # SweepMe! device class # Type: Scope # Device: Keysight EXR/MXR/UXR Series (tested on EXR only) -from EmptyDeviceClass import EmptyDevice +from pysweepme.EmptyDeviceClass import EmptyDevice import numpy as np import time as time + class Device(EmptyDevice): description = """ @@ -50,7 +50,7 @@ def __init__(self): self.variables = ["Time"] self.units = ["s"] - self.plottype = [True] # True to plot data + self.plottype = [True] # True to plot data self.savetype = [True] # True to save data self.port_manager = True @@ -61,49 +61,47 @@ def __init__(self): "timeout": 10.0, "delay": 1.0, } - self.commands = { - "Channel 1": "CH1", - "Channel 2": "CH2", - "Channel 3": "CH3", - "Channel 4": "CH4", - "External": "EXT", - "Line": "LINE", - "None": "NONE", - "Rising": "POS", - "Falling": "NEG", - } + "Channel 1": "CH1", + "Channel 2": "CH2", + "Channel 3": "CH3", + "Channel 4": "CH4", + "External": "EXT", + "Line": "LINE", + "None": "NONE", + "Rising": "POS", + "Falling": "NEG", + } def set_GUIparameter(self): GUIparameter = { - "SweepMode": ["None"], - - "TriggerSlope": ["As is", "Rising", "Falling"], - "TriggerSource": ["As is", "CHAN1", "CHAN2", "CHAN3", "CHAN4", "AUX", "LINE"], - # "TriggerCoupling": ["As is", "AC", "DC", "HF", "Auto level"], # not yet implemented - "TriggerLevel": 0, - "TriggerDelay": 0, - "TriggerTimeout": 2, - "TimeRange": ["Time range in s", "Time scale in s/div"], - "TimeRangeValue": 5e-4, - "TimeOffsetValue": 0.0, - "SamplingRate": ["10e+3", "100e+3", "1e+6", "10e+6", "100e+6", "1e+9","5e+9","10e+9","16e+9"], - "SamplingRateType":["BITS10: 10 Bits (16 GSa or Manual)","BITS11: 11 Bits (6.4 GSa)","BITS12: 12 Bits (3.2 GSa)","BITS13: 13 Bits (1.6 GSa)","BITS14: 14 Bits (800 MSa)","BITS15: 15 Bits (400 GSa)","BITS16: 16 Bits (200 GSa)","BITS16_4: 16 Bits (100 GSa)","BITS16_2: 16 Bits (50 GSa)"], - "Acquisition": ["Continuous", "Single"], - "Average": ["none", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"], - "VoltageRange": ["Voltage range in V"], - } + "SweepMode": ["None"], + + "TriggerSlope": ["As is", "Rising", "Falling"], + "TriggerSource": ["As is", "CHAN1", "CHAN2", "CHAN3", "CHAN4", "AUX", "LINE"], + # "TriggerCoupling": ["As is", "AC", "DC", "HF", "Auto level"], # not yet implemented + "TriggerLevel": 0, + "TriggerDelay": 0, + "TriggerTimeout": 2, + "TimeRange": ["Time range in s", "Time scale in s/div"], + "TimeRangeValue": 5e-4, + "TimeOffsetValue": 0.0, + "SamplingRate": ["10e+3", "100e+3", "1e+6", "10e+6", "100e+6", "1e+9","5e+9","10e+9","16e+9"], + "SamplingRateType":["BITS10: 10 Bits (16 GSa or Manual)","BITS11: 11 Bits (6.4 GSa)","BITS12: 12 Bits (3.2 GSa)","BITS13: 13 Bits (1.6 GSa)","BITS14: 14 Bits (800 MSa)","BITS15: 15 Bits (400 GSa)","BITS16: 16 Bits (200 GSa)","BITS16_4: 16 Bits (100 GSa)","BITS16_2: 16 Bits (50 GSa)"], + "Acquisition": ["Continuous", "Single"], + "Average": ["none", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"], + "VoltageRange": ["Voltage range in V"], + } - for i in range(1,5): + for i in range(1, 5): GUIparameter["Channel%i" % i] = True if i == 1 else False GUIparameter["Channel%i_Name" % i] = "CH%i" % i GUIparameter["Channel%i_Range" % i] = ["4e-2", "8e-2", "2e-1", "4e-1", "8e-1", "2", "4", "8", "20", "40", "80", "200"] GUIparameter["Channel%i_Offset" % i] = 0.0 return GUIparameter - def get_GUIparameter(self, parameter={}): #print(parameter) @@ -119,9 +117,9 @@ def get_GUIparameter(self, parameter={}): self.timerangevalue = float(parameter["TimeRangeValue"]) self.timeoffsetvalue = parameter["TimeOffsetValue"] self.samplingrate = parameter["SamplingRate"] - self.samplingratetype = parameter["SamplingRateType"] #reads the chosen "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + self.samplingratetype = parameter["SamplingRateType"] # reads the chosen "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth - if self.samplingratetype.startswith("BITS16_"): #chosen ADC resolution is taken from the GUI selection and modified so it can be used as the required parameter in the respective SCPI command + if self.samplingratetype.startswith("BITS16_"): # chosen ADC resolution is taken from the GUI selection and modified so it can be used as the required parameter in the respective SCPI command self.samplingratetype = self.samplingratetype[:8] else: self.samplingratetype = self.samplingratetype[:6] @@ -149,65 +147,61 @@ def get_GUIparameter(self, parameter={}): self.channel_divs[i] = self.channel_ranges[i] / 8 self.channel_offsets[i] = parameter["Channel%i_Offset" % i] - def initialize(self): # This driver does not use Reset yet so that user can do measurements with changing options manually - #self.port.write("*RST") + # self.port.write("*RST") if len(self.channels) == 0: raise Exception("Please select at least one channel to be read out") if int(10*float(self.triggertimeout)) % 2 != 0: + # values are multiplied by 10 to allow comparison operation in integer realm msg = "Trigger timeout can only be set in steps of 0.2s" raise Exception(msg) - #values are multiplied by 10 to allow comparison operation in integer realm - - #self.port.write("*IDN?") # Query device name - #print("ID Checkup") - #print(self.port.read()) - self.port.write("*CLS") # Clears all the event registers, and also clears the error queue. + # self.port.write("*IDN?") # Query device name + # print("ID Checkup") + # print(self.port.read()) + + self.port.write("*CLS") # Clears all the event registers, and also clears the error queue. - self.port.write(":WAV:FORM ASC") # sets encoding to ASCii insetad of BYTE or WORD; BYTE and WORD would allow for a much quicker data transfer, but values would have to be processed first before use. - self.port.write(":WAV:TYPE RAW") # sets data type to RAW, transmitting only true sampling points, no interpolation + self.port.write(":WAV:FORM ASC") # sets encoding to ASCii insetad of BYTE or WORD; BYTE and WORD would allow for a much quicker data transfer, but values would have to be processed first before use. + self.port.write(":WAV:TYPE RAW") # sets data type to RAW, transmitting only true sampling points, no interpolation def configure(self): - - ### Acquisition ### - self.port.write(":ACQ:MODE RTIM") #use real time aquisition + # Acquisition # + self.port.write(":ACQ:MODE RTIM") # use real time aquisition if self.average =="none": - self.port.write(":ACQ:AVER OFF") #diable averaging of triggered shots + self.port.write(":ACQ:AVER OFF") # diable averaging of triggered shots else: - self.port.write(":ACQ:AVER ON") #enabling averaging of triggered shots - self.port.write(":ACQ:AVER:COUN %s" %self.average) #setting the amount of triggered shots to be used for averaging + self.port.write(":ACQ:AVER ON") # enabling averaging of triggered shots + self.port.write(":ACQ:AVER:COUN %s" %self.average) # setting the amount of triggered shots to be used for averaging - self.port.write(":ACQ:POIN:ANAL AUTO") #set the Memory Depth to AUTO to give priority to sampling rate, mnanual p. 326 + self.port.write(":ACQ:POIN:ANAL AUTO") # set the Memory Depth to AUTO to give priority to sampling rate, mnanual p. 326 if self.samplingratetype.startswith("BITS10"): - self.port.write(":ACQ:SRAT:ANAL %s" %self.samplingrate) #sets the sampling rate, memory auto-adjusts accordingly. Only allowed with 10bits ADC setting!!! + self.port.write(":ACQ:SRAT:ANAL %s" %self.samplingrate) # sets the sampling rate, memory auto-adjusts accordingly. Only allowed with 10bits ADC setting!!! else: - self.port.write(":ACQ:SRAT:ANAL AUTO") #sampling rate to AUTO for all ADC interpolation options - self.port.write(":ACQ:ADCR %s" %self.samplingratetype) #sets the "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + self.port.write(":ACQ:SRAT:ANAL AUTO") # sampling rate to AUTO for all ADC interpolation options + self.port.write(":ACQ:ADCR %s" %self.samplingratetype) # sets the "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth - - self.port.write(":TIM:REF CENTER") #sets the timebase reference to center; might be a future GUI option to allow for left or right border choice? + self.port.write(":TIM:REF CENTER") #sets the timebase reference to center; might be a future GUI option to allow for left or right border choice? ##self.port.write(":ACQ:AVER %s" %self.average) # set averages - - ### Trigger ### + # Trigger # # setting trigger source and level if self.triggersource == "As is": pass else: - self.port.write(":TRIG:EDGE:SOUR %s" % self.triggersource) # set trigger source - self.port.write(":TRIG:LEV %s,%s" % (self.triggersource, self.triggerlevel)) # set trigger level + self.port.write(":TRIG:EDGE:SOUR %s" % self.triggersource) # set trigger source + self.port.write(":TRIG:LEV %s,%s" % (self.triggersource, self.triggerlevel)) # set trigger level - if self.triggerlevel == 0: # if no specific trigger level desired, - self.port.write(":TRIG:LEV:FIFT") # sets the trigger level at 50% + if self.triggerlevel == 0: # if no specific trigger level desired, + self.port.write(":TRIG:LEV:FIFT") # sets the trigger level at 50% - if self.triggerslope == "As is": # set trigger slope + if self.triggerslope == "As is": # set trigger slope pass elif self.triggerslope== "Rising": self.port.write(":TRIG:EDGE:SLOP POS") @@ -215,27 +209,26 @@ def configure(self): self.port.write(":TRIG:EDGE:SLOP NEG") self.port.write(":TRIG:DEL:TDEL:TIME %s" % self.triggerdelay) # sets trigger delay; very usefull if scope is combined in a sweepme sequence with other instruments - self.port.write(":TRIG:SWE TRIG") #set trigger sweep mode to TRIGGERED - - ### Time range ### + self.port.write(":TRIG:SWE TRIG") # set trigger sweep mode to TRIGGERED + # Time range # if self.timerange == "Time range in s": - self.port.write(":TIM:RANG %s" %self.timerangevalue) #set timebase range + self.port.write(":TIM:RANG %s" %self.timerangevalue) # set timebase range elif self.timerange == "Time scale in s/div": - self.port.write(":TIM:SCAL %s" %self.timerangevalue) #set timebase scale + self.port.write(":TIM:SCAL %s" %self.timerangevalue) # set timebase scale if self.timeoffsetvalue == "As is": pass else: - self.port.write(":TIM:POS %s" %self.timeoffsetvalue) #set timebase offset + self.port.write(":TIM:POS %s" %self.timeoffsetvalue) # set timebase offset - for i in range(1,5): #makes sure that only activated channels are displayed + for i in range(1, 5): # makes sure that only activated channels are displayed if i in self.channels: - self.port.write(":CHAN%s:DISP ON" % i) # turn on selected channels - self.port.write(":CHAN%s:SCAL %s" % (i, self.channel_divs[i])) # scale of channel - self.port.write(":CHAN%s:OFFS %s" % (i, self.channel_offsets[i])) # define offset of channel + self.port.write(":CHAN%s:DISP ON" % i) # turn on selected channels + self.port.write(":CHAN%s:SCAL %s" % (i, self.channel_divs[i])) # scale of channel + self.port.write(":CHAN%s:OFFS %s" % (i, self.channel_offsets[i])) # define offset of channel else: - self.port.write(":CHAN%s:DISP OFF" % i) # turn off unselected channels + self.port.write(":CHAN%s:DISP OFF" % i) # turn off unselected channels def apply(self): pass @@ -243,24 +236,24 @@ def apply(self): def measure(self): if self.average != "none" and self.acquisition.startswith("Cont"): - self.port.write(":CDIS") # clear display to reset the averaging counter when using continous trigger + self.port.write(":CDIS") # clear display to reset the averaging counter when using continous trigger time.sleep(0.3) if self.acquisition.startswith("Single"): - self.port.write(":SING") #performs single acquisition + self.port.write(":SING") # performs single acquisition elif self.acquisition.startswith("Cont"): - self.port.write(":RUN") # run continuous aquisition; not required when using single trigger + self.port.write(":RUN") # run continuous aquisition; not required when using single trigger time.sleep(float(self.triggerdelay)) trigcounter = 0 while True: - self.port.write(":ADER?") #check if trigger aquisition was successfull; if averaging is enabled, it will return 1 only when all average samples have been taken + self.port.write(":ADER?") # check if trigger aquisition was successfull; if averaging is enabled, it will return 1 only when all average samples have been taken triggerstat=int(self.port.read()) print("triggerstatus:", triggerstat) - if triggerstat == 0 and trigcounter < int(round(float(self.triggertimeout),1)*5): #if no trigger was aquired, wait loops in 0.2s increments + if triggerstat == 0 and trigcounter < int(round(float(self.triggertimeout),1)*5): #if no trigger was aquired, wait loops in 0.2s increments trigcounter += 1 time.sleep(0.2) - elif trigcounter >= int(round(float(self.triggertimeout),1)*5): #timeout in trigger wait loop, stops the aqusition + elif trigcounter >= int(round(float(self.triggertimeout),1)*5): #timeout in trigger wait loop, stops the aqusition self.port.write(":STOP") if self.average == "none": msg = "Oscilloscope could not trigger before timeout" @@ -272,16 +265,16 @@ def measure(self): break if self.acquisition.startswith("Cont"): - self.port.write(":STOP") # stop continuous aquisition; not required when using single trigger + self.port.write(":STOP") # stop continuous aquisition; not required when using single trigger - slot = 0 # run variable for data sorting + slot = 0 # run variable for data sorting - time.sleep(0.2) - self.port.write(":WAV:PRE?") # retrieving the waveform preamble + self.port.write(":WAV:PRE?") # retrieving the waveform preamble time.sleep(0.2) - #This section retrieves the preamble which describes all properties of the stroes waveform. While not all attributes are used, they remain included for debugging purposes. + # This section retrieves the preamble which describes all properties of the stroes waveform. + # While not all attributes are used, they remain included for debugging purposes. preamble = self.port.read().split(",") wav_format=preamble[0] acq_mode=preamble[1] @@ -294,28 +287,27 @@ def measure(self): y_orig=float(preamble[8]) y_ref=float(preamble[9]) #print ("wav_format:", wav_format, "acq_mode:", acq_mode, "numberpoints:", numberpoints, "av_count:", av_count, "x_inc:", x_inc, "x_org:", x_orig, "x_ref:", x_ref, "y_inc:", y_inc, "y_org:", y_orig, "y_ref:", y_ref) - - + for i in self.channels: - self.port.write(":WAV:SOUR CHAN%s" % i) # select channel to be read + self.port.write(":WAV:SOUR CHAN%s" % i) # select channel to be read - if slot== 0: # only for first measurement - channels = len(self.channels) # number of measured channels + if slot == 0: # only for first measurement + channels = len(self.channels) # number of measured channels self.voltages = np.zeros((numberpoints, len(self.channels))) # generate empty array of correct size for channels + data - self.timecode = np.linspace(x_orig, (x_orig+x_inc*numberpoints), numberpoints) # generate linear time array FROM, TO, STEPSAMOUNT + self.timecode = np.linspace(x_orig, (x_orig+x_inc*numberpoints), numberpoints) # generate linear time array FROM, TO, STEPSAMOUNT - self.port.write(":WAV:DATA?;*OPC") # retrieve waveform values from scope - time.sleep(0.2) # give scope time to prepare waveform data for download - datapoints = self.port.read().split(",") # read values from scope + self.port.write(":WAV:DATA?;*OPC") # retrieve waveform values from scope + time.sleep(0.2) # give scope time to prepare waveform data for download + datapoints = self.port.read().split(",") # read values from scope - opccounter = 0 # set counter for OPC loop back to zero + opccounter = 0 # set counter for OPC loop back to zero while True: - self.port.write("*OPC?") # query scope in loop whether [OP]eration of waveform data tranmission is [C]ompleted + self.port.write("*OPC?") # query scope in loop whether [OP]eration of waveform data tranmission is [C]ompleted completed = int(self.port.read()) - if completed == 0 and opccounter < 25: # loop time hardcoded to 5s + if completed == 0 and opccounter < 25: # loop time hardcoded to 5s opccounter += 1 time.sleep(0.2) else: @@ -323,13 +315,12 @@ def measure(self): data = [] for i in np.arange(numberpoints): - data.append(datapoints[i]) # put waveform values of current channel into data output list + data.append(datapoints[i]) # put waveform values of current channel into data output list - data = np.array(data) # convert list to data array + data = np.array(data) # convert list to data array - self.voltages[:, slot] = data # inputs voltage data for channel i into correct column of data array - slot += 1 # set correct column for next channel - + self.voltages[:, slot] = data # inputs voltage data for channel i into correct column of data array + slot += 1 # set correct column for next channel def call(self): - return [self.timecode] + [self.voltages[:,i] for i in range(self.voltages.shape[1])] + return [self.timecode] + [self.voltages[:,i] for i in range(self.voltages.shape[1])] diff --git a/src/Keysight_EXR/run.ini b/src/Keysight_EXR/run.ini deleted file mode 100644 index a1ece74a..00000000 --- a/src/Keysight_EXR/run.ini +++ /dev/null @@ -1,6 +0,0 @@ -[run] -version = 1.5.6.18 -started = 1.5.6.18 -tested = failed -hash = 2ed94d021b624737d51d1aec95bdb584 - From 73c9128572324cea036e10e4fbcf4c53d7e32656 Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Tue, 20 Aug 2024 16:08:29 +0200 Subject: [PATCH 4/9] Driver name changed to "Scope-Keysight_EXR" --- src/{Keysight_EXR => Scope-Keysight_EXR}/license.txt | 0 src/{Keysight_EXR => Scope-Keysight_EXR}/main.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{Keysight_EXR => Scope-Keysight_EXR}/license.txt (100%) rename src/{Keysight_EXR => Scope-Keysight_EXR}/main.py (100%) diff --git a/src/Keysight_EXR/license.txt b/src/Scope-Keysight_EXR/license.txt similarity index 100% rename from src/Keysight_EXR/license.txt rename to src/Scope-Keysight_EXR/license.txt diff --git a/src/Keysight_EXR/main.py b/src/Scope-Keysight_EXR/main.py similarity index 100% rename from src/Keysight_EXR/main.py rename to src/Scope-Keysight_EXR/main.py From f4cd7d60e767b49c5674bde6af2b854cd3f2cd09 Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Tue, 20 Aug 2024 16:16:24 +0200 Subject: [PATCH 5/9] Trigger coupling not yet supported and thus also commented in get_GUIparameter to make pysweepme test happy --- src/Scope-Keysight_EXR/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Scope-Keysight_EXR/main.py b/src/Scope-Keysight_EXR/main.py index a22643c8..e4f6fdca 100644 --- a/src/Scope-Keysight_EXR/main.py +++ b/src/Scope-Keysight_EXR/main.py @@ -107,7 +107,7 @@ def get_GUIparameter(self, parameter={}): #print(parameter) self.triggersource = parameter["TriggerSource"] - self.triggercoupling = parameter["TriggerCoupling"] + # self.triggercoupling = parameter["TriggerCoupling"] # not yet implemented self.triggerslope = parameter["TriggerSlope"] self.triggerlevel = parameter["TriggerLevel"] self.triggerdelay = parameter["TriggerDelay"] From b942650679fdf97a8751d861a8638c227b710088 Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Wed, 21 Aug 2024 16:57:24 +0200 Subject: [PATCH 6/9] Support for new ADC resolution GUI field, ADC resolution options and commands restructured, formatting changes --- src/Scope-Keysight_EXR/main.py | 136 +++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 47 deletions(-) diff --git a/src/Scope-Keysight_EXR/main.py b/src/Scope-Keysight_EXR/main.py index e4f6fdca..04f51153 100644 --- a/src/Scope-Keysight_EXR/main.py +++ b/src/Scope-Keysight_EXR/main.py @@ -73,6 +73,18 @@ def __init__(self): "Rising": "POS", "Falling": "NEG", } + + self.adc_resolution_options = { + "10 Bits (16 GSa or Manual)": "BITS10", + "11 Bits (6.4 GSa)": "BITS11", + "12 Bits (3.2 GSa)": "BITS12", + "13 Bits (1.6 GSa)": "BITS13", + "14 Bits (800 MSa)": "BITS14", + "15 Bits (400 MSa)": "BITS15", + "16 Bits (200 MSa)": "BITS16", + "16 Bits (100 MSa)": "BITS16_4", + "16 Bits (50 MSa)": "BITS16_2", + } def set_GUIparameter(self): @@ -89,9 +101,10 @@ def set_GUIparameter(self): "TimeRangeValue": 5e-4, "TimeOffsetValue": 0.0, "SamplingRate": ["10e+3", "100e+3", "1e+6", "10e+6", "100e+6", "1e+9","5e+9","10e+9","16e+9"], - "SamplingRateType":["BITS10: 10 Bits (16 GSa or Manual)","BITS11: 11 Bits (6.4 GSa)","BITS12: 12 Bits (3.2 GSa)","BITS13: 13 Bits (1.6 GSa)","BITS14: 14 Bits (800 MSa)","BITS15: 15 Bits (400 GSa)","BITS16: 16 Bits (200 GSa)","BITS16_4: 16 Bits (100 GSa)","BITS16_2: 16 Bits (50 GSa)"], + "SamplingRateType": ["Samples per s"], + "ADCResolution": list(self.adc_resolution_options.keys()), "Acquisition": ["Continuous", "Single"], - "Average": ["none", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"], + "Average": ["None", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"], "VoltageRange": ["Voltage range in V"], } @@ -104,8 +117,7 @@ def set_GUIparameter(self): return GUIparameter def get_GUIparameter(self, parameter={}): - #print(parameter) - + self.triggersource = parameter["TriggerSource"] # self.triggercoupling = parameter["TriggerCoupling"] # not yet implemented self.triggerslope = parameter["TriggerSlope"] @@ -117,12 +129,13 @@ def get_GUIparameter(self, parameter={}): self.timerangevalue = float(parameter["TimeRangeValue"]) self.timeoffsetvalue = parameter["TimeOffsetValue"] self.samplingrate = parameter["SamplingRate"] - self.samplingratetype = parameter["SamplingRateType"] # reads the chosen "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth - - if self.samplingratetype.startswith("BITS16_"): # chosen ADC resolution is taken from the GUI selection and modified so it can be used as the required parameter in the respective SCPI command - self.samplingratetype = self.samplingratetype[:8] - else: - self.samplingratetype = self.samplingratetype[:6] + self.samplingratetype = parameter["SamplingRateType"] + + # used to set the ADC resolution to enable resolution increase at the cost of sampling rate and bandwidth + adc_resolution_selection = parameter["ADCResolution"] + + # required SCPI command parameter is taken according to selected ADC resolution via dictionary + self.adc_resolution = self.adc_resolution_options[adc_resolution_selection] self.acquisition = parameter["Acquisition"] self.average = parameter["Average"] @@ -133,7 +146,7 @@ def get_GUIparameter(self, parameter={}): self.channel_divs = {} self.channel_offsets = {} - for i in range(1,5): + for i in range(1, 5): if parameter["Channel%i" % i]: self.channels.append(i) @@ -163,32 +176,45 @@ def initialize(self): # print("ID Checkup") # print(self.port.read()) - self.port.write("*CLS") # Clears all the event registers, and also clears the error queue. - - self.port.write(":WAV:FORM ASC") # sets encoding to ASCii insetad of BYTE or WORD; BYTE and WORD would allow for a much quicker data transfer, but values would have to be processed first before use. - self.port.write(":WAV:TYPE RAW") # sets data type to RAW, transmitting only true sampling points, no interpolation + # Clears all the event registers, and also clears the error queue. + self.port.write("*CLS") + + # sets encoding to ASCii instead of BYTE or WORD; BYTE and WORD would allow for a much quicker data transfer, + # but values would have to be processed first before use. + self.port.write(":WAV:FORM ASC") + + # sets data type to RAW, transmitting only true sampling points, no interpolation + self.port.write(":WAV:TYPE RAW") def configure(self): # Acquisition # - self.port.write(":ACQ:MODE RTIM") # use real time aquisition + self.port.write(":ACQ:MODE RTIM") # use real time acquisition - if self.average =="none": - self.port.write(":ACQ:AVER OFF") # diable averaging of triggered shots + if self.average == "None": + self.port.write(":ACQ:AVER OFF") # disable averaging of triggered shots else: - self.port.write(":ACQ:AVER ON") # enabling averaging of triggered shots - self.port.write(":ACQ:AVER:COUN %s" %self.average) # setting the amount of triggered shots to be used for averaging - - self.port.write(":ACQ:POIN:ANAL AUTO") # set the Memory Depth to AUTO to give priority to sampling rate, mnanual p. 326 - if self.samplingratetype.startswith("BITS10"): - self.port.write(":ACQ:SRAT:ANAL %s" %self.samplingrate) # sets the sampling rate, memory auto-adjusts accordingly. Only allowed with 10bits ADC setting!!! + # enabling averaging of triggered shots + self.port.write(":ACQ:AVER ON") + + # setting the amount of triggered shots to be used for averaging + self.port.write(":ACQ:AVER:COUN %s" % self.average) + + # set the Memory Depth to AUTO to give priority to sampling rate, manual p. 326 + self.port.write(":ACQ:POIN:ANAL AUTO") + + if self.adc_resolution == "BITS10": + # sets the sampling rate, memory auto-adjusts accordingly. Only allowed with 10bits ADC setting!!! + self.port.write(":ACQ:SRAT:ANAL %s" % self.samplingrate) else: - self.port.write(":ACQ:SRAT:ANAL AUTO") # sampling rate to AUTO for all ADC interpolation options - self.port.write(":ACQ:ADCR %s" %self.samplingratetype) # sets the "sampling rate type", here: used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + # sampling rate to AUTO for all ADC interpolation options + self.port.write(":ACQ:SRAT:ANAL AUTO") - self.port.write(":TIM:REF CENTER") #sets the timebase reference to center; might be a future GUI option to allow for left or right border choice? - - ##self.port.write(":ACQ:AVER %s" %self.average) # set averages + # used to set the ADC resolution to enable resolution increase at the cost of samplingrate and bandwidth + self.port.write(":ACQ:ADCR %s" % self.adc_resolution) + + # sets the timebase reference to center; might be a future GUI option to allow for left or right border choice? + self.port.write(":TIM:REF CENTER") # Trigger # # setting trigger source and level @@ -203,26 +229,30 @@ def configure(self): if self.triggerslope == "As is": # set trigger slope pass - elif self.triggerslope== "Rising": + elif self.triggerslope == "Rising": self.port.write(":TRIG:EDGE:SLOP POS") elif self.triggerslope == "Falling": self.port.write(":TRIG:EDGE:SLOP NEG") - self.port.write(":TRIG:DEL:TDEL:TIME %s" % self.triggerdelay) # sets trigger delay; very usefull if scope is combined in a sweepme sequence with other instruments - self.port.write(":TRIG:SWE TRIG") # set trigger sweep mode to TRIGGERED + # sets trigger delay; very useful if scope is combined in a sweepme sequence with other instruments + self.port.write(":TRIG:DEL:TDEL:TIME %s" % self.triggerdelay) + + # set trigger sweep mode to TRIGGERED + self.port.write(":TRIG:SWE TRIG") # Time range # if self.timerange == "Time range in s": - self.port.write(":TIM:RANG %s" %self.timerangevalue) # set timebase range + self.port.write(":TIM:RANG %s" % self.timerangevalue) # set timebase range elif self.timerange == "Time scale in s/div": - self.port.write(":TIM:SCAL %s" %self.timerangevalue) # set timebase scale + self.port.write(":TIM:SCAL %s" % self.timerangevalue) # set timebase scale if self.timeoffsetvalue == "As is": pass else: - self.port.write(":TIM:POS %s" %self.timeoffsetvalue) # set timebase offset + self.port.write(":TIM:POS %s" % self.timeoffsetvalue) # set timebase offset - for i in range(1, 5): # makes sure that only activated channels are displayed + # makes sure that only activated channels are displayed + for i in range(1, 5): if i in self.channels: self.port.write(":CHAN%s:DISP ON" % i) # turn on selected channels self.port.write(":CHAN%s:SCAL %s" % (i, self.channel_divs[i])) # scale of channel @@ -236,24 +266,30 @@ def apply(self): def measure(self): if self.average != "none" and self.acquisition.startswith("Cont"): - self.port.write(":CDIS") # clear display to reset the averaging counter when using continous trigger + self.port.write(":CDIS") # clear display to reset the averaging counter when using continuous trigger time.sleep(0.3) if self.acquisition.startswith("Single"): self.port.write(":SING") # performs single acquisition elif self.acquisition.startswith("Cont"): - self.port.write(":RUN") # run continuous aquisition; not required when using single trigger + self.port.write(":RUN") # run continuous acquisition; not required when using single trigger time.sleep(float(self.triggerdelay)) trigcounter = 0 while True: - self.port.write(":ADER?") # check if trigger aquisition was successfull; if averaging is enabled, it will return 1 only when all average samples have been taken - triggerstat=int(self.port.read()) + # check if trigger acquisition was successful; + # if averaging is enabled, it will return 1 only when all average samples have been taken + self.port.write(":ADER?") + triggerstat = int(self.port.read()) print("triggerstatus:", triggerstat) - if triggerstat == 0 and trigcounter < int(round(float(self.triggertimeout),1)*5): #if no trigger was aquired, wait loops in 0.2s increments + + # if no trigger was acquired, wait loops in 0.2s increments + if triggerstat == 0 and trigcounter < int(round(float(self.triggertimeout),1)*5): trigcounter += 1 time.sleep(0.2) - elif trigcounter >= int(round(float(self.triggertimeout),1)*5): #timeout in trigger wait loop, stops the aqusition + + # timeout in trigger wait loop, stops the acquisition + elif trigcounter >= int(round(float(self.triggertimeout),1)*5): self.port.write(":STOP") if self.average == "none": msg = "Oscilloscope could not trigger before timeout" @@ -265,7 +301,7 @@ def measure(self): break if self.acquisition.startswith("Cont"): - self.port.write(":STOP") # stop continuous aquisition; not required when using single trigger + self.port.write(":STOP") # stop continuous acquisition; not required when using single trigger slot = 0 # run variable for data sorting @@ -286,7 +322,10 @@ def measure(self): y_inc=float(preamble[7]) y_orig=float(preamble[8]) y_ref=float(preamble[9]) - #print ("wav_format:", wav_format, "acq_mode:", acq_mode, "numberpoints:", numberpoints, "av_count:", av_count, "x_inc:", x_inc, "x_org:", x_orig, "x_ref:", x_ref, "y_inc:", y_inc, "y_org:", y_orig, "y_ref:", y_ref) + + # print ("wav_format:", wav_format, "acq_mode:", acq_mode, "numberpoints:", numberpoints, + # "av_count:", av_count, "x_inc:", x_inc, "x_org:", x_orig, "x_ref:", x_ref, "y_inc:", y_inc, + # "y_org:", y_orig, "y_ref:", y_ref) for i in self.channels: @@ -294,9 +333,11 @@ def measure(self): if slot == 0: # only for first measurement channels = len(self.channels) # number of measured channels - self.voltages = np.zeros((numberpoints, len(self.channels))) # generate empty array of correct size for channels + data + # generate empty array of correct size for channels + data + self.voltages = np.zeros((numberpoints, len(self.channels))) - self.timecode = np.linspace(x_orig, (x_orig+x_inc*numberpoints), numberpoints) # generate linear time array FROM, TO, STEPSAMOUNT + # generate linear time array FROM, TO, STEPSAMOUNT + self.timecode = np.linspace(x_orig, (x_orig+x_inc*numberpoints), numberpoints) self.port.write(":WAV:DATA?;*OPC") # retrieve waveform values from scope time.sleep(0.2) # give scope time to prepare waveform data for download @@ -305,7 +346,8 @@ def measure(self): opccounter = 0 # set counter for OPC loop back to zero while True: - self.port.write("*OPC?") # query scope in loop whether [OP]eration of waveform data tranmission is [C]ompleted + # query scope in loop whether [OP]eration of waveform data tranmission is [C]ompleted + self.port.write("*OPC?") completed = int(self.port.read()) if completed == 0 and opccounter < 25: # loop time hardcoded to 5s opccounter += 1 From 5fdf1faff031fbb286bd2ebbd40fdeb454d35bbd Mon Sep 17 00:00:00 2001 From: Code-Craftsman-Christian Date: Fri, 23 Aug 2024 12:23:34 +0200 Subject: [PATCH 7/9] initial driver release for HP/Agilent/Keysight E3631A Triple-Output PSU Based on the E3632A driver but some changes were necessary to adjust to the E3631A command set. The so-called "TRACK" mode is available as well as a channel selection, synchronizing the +25V and -25V to a common absolute voltage value for e.g. OPA supply. In TRACK mode, the driver is programmed to accept positive voltage values to output this on the +25V terminal and the respective negative equivalent to the -25V terminal. As usual, some sanity checks including error messages are implemented as well regarding maximum voltage and current limits and polarity of input. --- src/SMU-Keysight_E3631A/main.py | 202 ++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/SMU-Keysight_E3631A/main.py diff --git a/src/SMU-Keysight_E3631A/main.py b/src/SMU-Keysight_E3631A/main.py new file mode 100644 index 00000000..63e2530b --- /dev/null +++ b/src/SMU-Keysight_E3631A/main.py @@ -0,0 +1,202 @@ +# This Device Class is published under the terms of the MIT License. +# Required Third Party Libraries, which are included in the Device Class +# package for convenience purposes, may have a different license. You can +# find those in the corresponding folders or contact the maintainer. +# +# MIT License +# +# Copyright (c) 2024 SweepMe! GmbH (sweep-me.net) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# SweepMe! driver +# * Module: SMU +# * Instrument: Agilent/Keysight E3631A + +import time + +# from pysweepme.ErrorMessage import error +from pysweepme.EmptyDeviceClass import EmptyDevice + + +class Device(EmptyDevice): + """ + description = +

Notes:

+
    +
  • COM Port untested as of 20240807
  • +
  • Calibration commands not implemented
  • +
  • -
  • +
+ """ + + def __init__(self): + + super().__init__() + #EmptyDevice.__init__(self) + + self.shortname = "E3631A" + + # remains here for compatibility with v1.5.3 + self.multichannel = ["P6V", "P25V", "N25V", "TRACK25V"] + + self.variables = ["Voltage", "Current"] + self.units = ["V", "A"] + self.plottype = [True, True] # True to plot data + self.savetype = [True, True] # True to save data + + self.port_manager = True + self.port_types = ["COM", "GPIB"] + + self.port_properties = {"timeout": 3, + } + + def set_GUIparameter(self): + + gui_parameter = { + "SweepMode": ["Voltage in V"], + "Channel": ["P6V", "P25V", "N25V", "TRACK25V"], + "RouteOut": ["Front"], + "Compliance": 1, + #"RangeVoltage": ["15 V / 7 A", "30 V / 4 A"], #no voltage range per channel on the E3631A + } + + return gui_parameter + + def get_GUIparameter(self, parameter={}): + self.port_string = parameter["Port"] + self.source = parameter["SweepMode"] + #self.route_out = parameter["RouteOut"] + self.currentlimit = parameter["Compliance"] + + self.device = parameter['Device'] + self.channel = parameter['Channel'] + + def initialize(self): + #self.port.write("*IDN?") + #identifier = self.port.read() + # print("Identifier:", identifier) # can be used to check the instrument + + self.port.write("*CLS") + + def configure(self): + + # NOT AVAILABLE ON E3631A: self.port.write("VOLT:PROT:STAT OFF") # output voltage protection disabled + # NOT AVAILABLE ON E3631A: self.port.write("CURR:PROT:STAT OFF") # output current protection disabled + + # NOT AVAILABLE ON E3631A: hardcoded overvoltage protection limit, causes switch-off of channel; set to PSU protection default value + #self.port.write("VOLT:PROT:LEV 32") + + # NOT AVAILABLE ON E3631A: hardcoded overcurrent protection limit, causes switch-off of channel; set to PSU protection default value + #self.port.write("CURR:PROT:LEV 7.5") + + ## checking if requested compliance is within PSU capabilities and enabling TRACK mode if channel option was selected in GUI" + if self.channel=="P6V": + if float(self.currentlimit) > 5: + msg = "Lower compliance limit to max 5 A" + raise Exception(msg) + + elif self.channel=="P25V" or self.channel=="N25V" or self.channel=="TRACK25V": + if float(self.currentlimit) > 1: + msg = "Lower compliance limit to max 1 A" + raise Exception(msg) + if self.channel=="TRACK25V": + self.port.write("OUTPUT:TRAC:STAT ON") #activate sync of both 25V channels + else: + self.port.write("OUTPUT:TRAC:STAT OFF") #make sure, sync of +25V and -25V channel is disabled + else: + msg = "The input channel selection is not valid." + raise Exception(msg) + + self.select_channel() + + self.port.write("CURR:LEV:IMM %1.4f" % float(self.currentlimit)) #set compliance limit for selected channel. + + def unconfigure(self): + self.select_channel() + + self.port.write("VOLT:LEV MIN") #Safety measure, setting voltage back to minimum before output switch-off. hint: this command differs slightly from the one used in the E3632A driver + + # if self.port_string.startswith("COM"): + # self.port.write("SYST:LOC") # On the E3631A, ONLY ALLOWED WITH RS232 + + def deinitialize(self): + pass + + def poweron(self): + self.port.write("OUTP:STAT ON") + + def poweroff(self): + self.port.write("OUTP:STAT OFF") + + def apply(self): + if self.channel=="P6V" and self.value > 6: + msg = "Requested voltage out of range for this channel (max. 6 V)" + raise Exception(msg) + + if self.channel=="P25V" or self.channel=="TRACK25V": + if abs(self.value) > 25: + msg = "Requested voltage out of range for this channel (max. +25 V)" + raise Exception(msg) + elif self.channel=="TRACK25V" and self.value < 0: + msg = "Use positive values only in TRACK mode to request symmetric voltage on both channels." + raise Exception(msg) + + if self.channel=="N25V": + if abs(self.value) > 25: + msg = "Requested voltage out of range for this channel (max. -25 V)" + raise Exception(msg) + elif self.value > 0: + msg = "Positive voltages not possible on this channel (N25V)." + raise Exception(msg) + + self.select_channel() + self.port.write("VOLT:LEV:IMM %1.4f" % float(self.value)) + + def measure(self): + self.select_channel() + self.port.write("MEAS:VOLT?") + self.v = float(self.port.read()) + self.port.write("MEAS:CURR?") + self.i = float(self.port.read()) + + def call(self): + return [self.v, self.i] + + def display_off(self): + # For further use. + + self.port.write("DISP:STAT OFF") + # wait for display shutdown procedure to complete + # time.sleep(0.5) + + def display_on(self): + # For further use. + + self.port.write("DISP:STAT ON") + # wait for display switch-on procedure to complete + # time.sleep(0.5) + + def select_channel(self): + #selects the current channel as the receipt for following SCPI configuration commands + print(self.channel) + if self.channel=="TRACK25V": + self.port.write("INST:SEL P25V") #when in TRACK mode (synced +/-25V channels), voltage on both channels can be set by setting P25V channel or N25V channel arbitrarily. Here, P25V is used to positive values can be used as the negative channel only accepts negative voltage values. + else: + self.port.write("INST:SEL %s" % self.channel) #select channel to configure From ec3e46b827a931962e3dbd61f8bcdff82d44972e Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Fri, 23 Aug 2024 15:13:46 +0200 Subject: [PATCH 8/9] Keysight E3631A -> formatting changes, channel names introduced --- src/SMU-Keysight_E3631A/main.py | 87 +++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/SMU-Keysight_E3631A/main.py b/src/SMU-Keysight_E3631A/main.py index 63e2530b..0d884cc1 100644 --- a/src/SMU-Keysight_E3631A/main.py +++ b/src/SMU-Keysight_E3631A/main.py @@ -49,13 +49,9 @@ class Device(EmptyDevice): def __init__(self): super().__init__() - #EmptyDevice.__init__(self) self.shortname = "E3631A" - # remains here for compatibility with v1.5.3 - self.multichannel = ["P6V", "P25V", "N25V", "TRACK25V"] - self.variables = ["Voltage", "Current"] self.units = ["V", "A"] self.plottype = [True, True] # True to plot data @@ -64,17 +60,25 @@ def __init__(self): self.port_manager = True self.port_types = ["COM", "GPIB"] - self.port_properties = {"timeout": 3, - } + self.port_properties = { + "timeout": 3, + } + + self.channels_commands = { + "+6V": "P6V", + "+25V": "P25V", + "-25V": "N25V", + "Synced +/-25V": "TRACK25V", # only a positive voltage is handed over + } def set_GUIparameter(self): gui_parameter = { "SweepMode": ["Voltage in V"], - "Channel": ["P6V", "P25V", "N25V", "TRACK25V"], + "Channel": list(self.channels_commands.keys()), "RouteOut": ["Front"], "Compliance": 1, - #"RangeVoltage": ["15 V / 7 A", "30 V / 4 A"], #no voltage range per channel on the E3631A + # "RangeVoltage": ["15 V / 7 A", "30 V / 4 A"], # no voltage range per channel on the E3631A } return gui_parameter @@ -86,11 +90,13 @@ def get_GUIparameter(self, parameter={}): self.currentlimit = parameter["Compliance"] self.device = parameter['Device'] - self.channel = parameter['Channel'] + + channel_selection = parameter['Channel'] + self.channel = self.channels_commands[channel_selection] def initialize(self): - #self.port.write("*IDN?") - #identifier = self.port.read() + # self.port.write("*IDN?") + # identifier = self.port.read() # print("Identifier:", identifier) # can be used to check the instrument self.port.write("*CLS") @@ -100,38 +106,43 @@ def configure(self): # NOT AVAILABLE ON E3631A: self.port.write("VOLT:PROT:STAT OFF") # output voltage protection disabled # NOT AVAILABLE ON E3631A: self.port.write("CURR:PROT:STAT OFF") # output current protection disabled - # NOT AVAILABLE ON E3631A: hardcoded overvoltage protection limit, causes switch-off of channel; set to PSU protection default value - #self.port.write("VOLT:PROT:LEV 32") + # NOT AVAILABLE ON E3631A: + # hardcoded overvoltage protection limit, causes switch-off of channel; set to PSU protection default value + # self.port.write("VOLT:PROT:LEV 32") - # NOT AVAILABLE ON E3631A: hardcoded overcurrent protection limit, causes switch-off of channel; set to PSU protection default value - #self.port.write("CURR:PROT:LEV 7.5") + # NOT AVAILABLE ON E3631A: + # hardcoded overcurrent protection limit, causes switch-off of channel; set to PSU protection default value + # self.port.write("CURR:PROT:LEV 7.5") - ## checking if requested compliance is within PSU capabilities and enabling TRACK mode if channel option was selected in GUI" - if self.channel=="P6V": + # checking if requested compliance is within PSU capabilities and + # enabling TRACK mode if channel option was selected in GUI" + if self.channel == "P6V": if float(self.currentlimit) > 5: msg = "Lower compliance limit to max 5 A" raise Exception(msg) - elif self.channel=="P25V" or self.channel=="N25V" or self.channel=="TRACK25V": + elif self.channel == "P25V" or self.channel == "N25V" or self.channel == "TRACK25V": if float(self.currentlimit) > 1: msg = "Lower compliance limit to max 1 A" raise Exception(msg) - if self.channel=="TRACK25V": - self.port.write("OUTPUT:TRAC:STAT ON") #activate sync of both 25V channels + if self.channel == "TRACK25V": + self.port.write("OUTPUT:TRAC:STAT ON") # activate sync of both 25V channels else: - self.port.write("OUTPUT:TRAC:STAT OFF") #make sure, sync of +25V and -25V channel is disabled + self.port.write("OUTPUT:TRAC:STAT OFF") # make sure, sync of +25V and -25V channel is disabled else: msg = "The input channel selection is not valid." raise Exception(msg) self.select_channel() - - self.port.write("CURR:LEV:IMM %1.4f" % float(self.currentlimit)) #set compliance limit for selected channel. + # set compliance limit for selected channel. + self.port.write("CURR:LEV:IMM %1.4f" % float(self.currentlimit)) def unconfigure(self): self.select_channel() - - self.port.write("VOLT:LEV MIN") #Safety measure, setting voltage back to minimum before output switch-off. hint: this command differs slightly from the one used in the E3632A driver + + # Safety measure, setting voltage back to minimum before output switch-off. hint: this command differs + # slightly from the one used in the E3632A driver + self.port.write("VOLT:LEV MIN") # if self.port_string.startswith("COM"): # self.port.write("SYST:LOC") # On the E3631A, ONLY ALLOWED WITH RS232 @@ -146,19 +157,19 @@ def poweroff(self): self.port.write("OUTP:STAT OFF") def apply(self): - if self.channel=="P6V" and self.value > 6: + if self.channel == "P6V" and self.value > 6: msg = "Requested voltage out of range for this channel (max. 6 V)" raise Exception(msg) - if self.channel=="P25V" or self.channel=="TRACK25V": + if self.channel == "P25V" or self.channel == "TRACK25V": if abs(self.value) > 25: msg = "Requested voltage out of range for this channel (max. +25 V)" raise Exception(msg) - elif self.channel=="TRACK25V" and self.value < 0: + elif self.channel == "TRACK25V" and self.value < 0: msg = "Use positive values only in TRACK mode to request symmetric voltage on both channels." raise Exception(msg) - if self.channel=="N25V": + if self.channel == "N25V": if abs(self.value) > 25: msg = "Requested voltage out of range for this channel (max. -25 V)" raise Exception(msg) @@ -180,23 +191,27 @@ def call(self): return [self.v, self.i] def display_off(self): - # For further use. self.port.write("DISP:STAT OFF") # wait for display shutdown procedure to complete # time.sleep(0.5) def display_on(self): - # For further use. self.port.write("DISP:STAT ON") # wait for display switch-on procedure to complete # time.sleep(0.5) def select_channel(self): - #selects the current channel as the receipt for following SCPI configuration commands - print(self.channel) - if self.channel=="TRACK25V": - self.port.write("INST:SEL P25V") #when in TRACK mode (synced +/-25V channels), voltage on both channels can be set by setting P25V channel or N25V channel arbitrarily. Here, P25V is used to positive values can be used as the negative channel only accepts negative voltage values. + """Selects the current channel as the receipt for following SCPI configuration commands. + """ + + if self.channel == "TRACK25V": + # when in TRACK mode (synced +/-25V channels), voltage on both channels can be set by + # setting P25V channel or N25V channel arbitrarily. + # Here, P25V is used to positive values can be used + # as the negative channel only accepts negative voltage values. + self.port.write("INST:SEL P25V") else: - self.port.write("INST:SEL %s" % self.channel) #select channel to configure + # select channel to configure + self.port.write("INST:SEL %s" % self.channel) From 73a25d066c3fd5e05440ad81da1a7fce11dc7397 Mon Sep 17 00:00:00 2001 From: Axel Fischer Date: Mon, 26 Aug 2024 14:37:47 +0200 Subject: [PATCH 9/9] Keysight E3631A -> only if the addressed channel changes, it is updated by sending a command --- src/SMU-Keysight_E3631A/main.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/SMU-Keysight_E3631A/main.py b/src/SMU-Keysight_E3631A/main.py index 0d884cc1..02fb31c3 100644 --- a/src/SMU-Keysight_E3631A/main.py +++ b/src/SMU-Keysight_E3631A/main.py @@ -101,6 +101,8 @@ def initialize(self): self.port.write("*CLS") + self.unique_identifier = self_device + "_" + self.port_string + "_channel" + def configure(self): # NOT AVAILABLE ON E3631A: self.port.write("VOLT:PROT:STAT OFF") # output voltage protection disabled @@ -206,12 +208,19 @@ def select_channel(self): """Selects the current channel as the receipt for following SCPI configuration commands. """ - if self.channel == "TRACK25V": - # when in TRACK mode (synced +/-25V channels), voltage on both channels can be set by - # setting P25V channel or N25V channel arbitrarily. - # Here, P25V is used to positive values can be used - # as the negative channel only accepts negative voltage values. - self.port.write("INST:SEL P25V") - else: - # select channel to configure - self.port.write("INST:SEL %s" % self.channel) + # only if a channel was not set so far or another channel is request, we change the channel + if (self.unique_identifier not in self.device_communication or + self.device_communication[self.unique_identifier] != self.channel): + + if self.channel == "TRACK25V": + # when in TRACK mode (synced +/-25V channels), voltage on both channels can be set by + # setting P25V channel or N25V channel arbitrarily. + # Here, P25V is used to positive values can be used + # as the negative channel only accepts negative voltage values. + self.port.write("INST:SEL P25V") + else: + # select channel to configure + self.port.write("INST:SEL %s" % self.channel) + + # updating the current channel information + self.device_communication[self.unique_identifier] = self.channel \ No newline at end of file