From 5505a553b564239d4ed725aa057d369ad5d277fa Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Thu, 22 Jan 2026 10:24:45 +1100 Subject: [PATCH 1/8] Update waveform function --- src/fixate/drivers/dso/agilent_mso_x.py | 45 ++++++++++++------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index 7d1fbf35..265993be 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -643,39 +643,38 @@ def waveform_preamble(self): preamble[labels[index]] = val return preamble - def waveform_values(self, signals, file_name="", file_type="csv"): + def waveform_values(self, signal, file_name="", file_type="csv"): """ - :param signals: + :param signal: The channel ie "1", "2", "3", "4", "MATH", "FUNC" :param file_name: If :param file_type: :return: """ - signals = self.digitize(signals) - return_vals = {} - for sig in signals: - return_vals[sig] = [] - results = return_vals[sig] - self.write(":WAV:SOUR {}".format(sig)) - self.write(":WAV:FORM BYTE") - self.write(":WAV:POIN:MODE RAW") - preamble = self.waveform_preamble() - data = self.retrieve_waveform_data() - for index, datum in enumerate(data): - time_val = index * preamble["x_increment"] - y_val = ( - preamble["y_origin"] - + (datum - preamble["y_reference"]) * preamble["y_increment"] - ) - results.append((time_val, y_val)) + signal = self.digitize(signal) + # digitize returns a list: + self.write(":WAV:SOUR {}".format(signal[0])) + self.write(":WAV:FORM BYTE") + self.write(":WAV:POIN:MODE RAW") + + preamble = self.waveform_preamble() + data = self.retrieve_waveform_data() + time_values = [] + values = [] + for index, datum in enumerate(data): + time_val = index * preamble["x_increment"] + y_val = ( + preamble["y_origin"] + + (datum - preamble["y_reference"]) * preamble["y_increment"] + ) + time_values.append(time_val) + values.append(y_val) if file_name and file_type == "csv": # Needs work for multiple references with open(file_name, "w") as f: f.write("x,y") - for label in sorted(preamble): - f.write(",{},{}".format(label, preamble[label])) f.write("\n") - for time_val, y_val in enumerate(results): + for time_val, y_val in zip(time_values, values): f.write( "{time_val},{voltage}\n".format( time_val=time_val, voltage=y_val @@ -683,7 +682,7 @@ def waveform_values(self, signals, file_name="", file_type="csv"): ) elif file_name and file_type == "bin": raise NotImplementedError("Binary Output not implemented") - return results + return time_values, values def retrieve_waveform_data(self): self.instrument.write(":WAV:DATA?") From 40c226e2893460b508581e34ab0ad0b7ddf72adb Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 4 Feb 2026 13:29:00 +1100 Subject: [PATCH 2/8] Updated waveform_values to not set the scope to run when getting data --- src/fixate/drivers/dso/agilent_mso_x.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index 265993be..a190fdf8 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -652,11 +652,16 @@ def waveform_values(self, signal, file_name="", file_type="csv"): :param file_type: :return: """ - signal = self.digitize(signal) - # digitize returns a list: - self.write(":WAV:SOUR {}".format(signal[0])) - self.write(":WAV:FORM BYTE") - self.write(":WAV:POIN:MODE RAW") + # Check if there is actually data to acquire: + # This line also makes the channel the source for the data export! + data_available = int( + self.query(":WAVeform:SOURce CHANnel" + str(signal) + ";POINTs?") + ) + if data_available == 0: + # No data is available + # Setting a channel to be a waveform source turns it on, so we need to turn it off now: + self.write(":CHANnel" + str(signal) + ":DISPlay OFF") + raise ValueError("No data is available") preamble = self.waveform_preamble() data = self.retrieve_waveform_data() From 667d4e039556de72e8c6aefbe51ed32276403fca Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 4 Feb 2026 14:31:08 +1100 Subject: [PATCH 3/8] Shift waveform to be centered on trigger --- src/fixate/drivers/dso/agilent_mso_x.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index a190fdf8..f255dc60 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -668,7 +668,7 @@ def waveform_values(self, signal, file_name="", file_type="csv"): time_values = [] values = [] for index, datum in enumerate(data): - time_val = index * preamble["x_increment"] + time_val = index * preamble["x_increment"] + preamble["x_origin"] y_val = ( preamble["y_origin"] + (datum - preamble["y_reference"]) * preamble["y_increment"] From f6923d3625eada3093b7396dd1c55a40f654f549 Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 11 Feb 2026 10:31:01 +1100 Subject: [PATCH 4/8] Update release notes --- docs/release-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 1d4a4ac4..e41a756f 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,6 +20,8 @@ Improvements - Sequencer logic now handles exceptions raised on sequence abort. GUI will no longer hang when a test raises an exception during a test abort. - Fix bug where DSOX1202G appeared to hang both the program and scope - LCR Driver now supports instruments reporting as Keysight or Agilent. Newer models of the LCR meter report as Keysight, whereas older models report as Agilent. +- DSO Driver function 'waveform_value' now returns a single channels x and y data as two separate lists, without re-acquiring the signal. This function should + now be called after performing singal acquisition. ************* Version 0.6.4 From 8c56d6ff8a12f114bba7a1d10840bf13b93388f8 Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 11 Feb 2026 10:39:33 +1100 Subject: [PATCH 5/8] Update docstring --- src/fixate/drivers/dso/agilent_mso_x.py | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index f255dc60..9ff2d2ba 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -645,13 +645,27 @@ def waveform_preamble(self): def waveform_values(self, signal, file_name="", file_type="csv"): """ - :param signal: - The channel ie "1", "2", "3", "4", "MATH", "FUNC" - :param file_name: - If - :param file_type: - :return: + Retrieves waveform data from the specified channel and optionally saves it to a file. + + This method queries the instrument for raw data points, scales them using + the waveform preamble (origin, increment, and reference values), and + converts them into time and voltage arrays. + + Args: + signal (str|int): The source channel (e.g., "1", "2", "MATH", "FUNC"). + file_name (str, optional): The path/name of the file to save data to. + Defaults to "", which skips file saving. + file_type (str, optional): The format for the output file. + Supported: "csv". Defaults to "csv". + + Returns: + tuple: A tuple containing (time_values, values) as lists of floats. + + Raises: + ValueError: If no data is available on the selected channel. + NotImplementedError: If an unsupported file_type is requested. """ + # Check if there is actually data to acquire: # This line also makes the channel the source for the data export! data_available = int( From 2c22ec5e7304214534fd24f981668173af0634a3 Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 4 Mar 2026 12:35:24 +1100 Subject: [PATCH 6/8] Update function to work with all acquire modes. --- src/fixate/drivers/dso/agilent_mso_x.py | 123 +++++++++++++++++------- 1 file changed, 87 insertions(+), 36 deletions(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index 9ff2d2ba..1cb86c90 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -652,7 +652,7 @@ def waveform_values(self, signal, file_name="", file_type="csv"): converts them into time and voltage arrays. Args: - signal (str|int): The source channel (e.g., "1", "2", "MATH", "FUNC"). + signal (str|int): The source channel (e.g., 1, "2"). file_name (str, optional): The path/name of the file to save data to. Defaults to "", which skips file saving. file_type (str, optional): The format for the output file. @@ -665,12 +665,44 @@ def waveform_values(self, signal, file_name="", file_type="csv"): ValueError: If no data is available on the selected channel. NotImplementedError: If an unsupported file_type is requested. """ + try: + # If the channel is not able to be converted to an int, then its almost definitely not an analogue source + # i.e. you might have requested "math" or "function" that is not supported by this method. + int(signal) + except ValueError: + raise ValueError( + "Please select an analog channel. Math or function channels are not supported." + ) + + # Exit early if the requested channel is not currently displayed: + ch_state = int(self.instrument.query(":CHANnel" + str(signal) + ":DISPlay?")) + if not ch_state: + raise ValueError("Requested channel is not active!") + + # Set the channel: + self.instrument.write(":WAVeform:SOURce CHANnel" + str(signal)) + # Explicitly set this to avoid confusion + self.instrument.write(":WAVeform:FORMat BYTE") + self.instrument.write(":WAVeform:UNSigned 0") + + # Pick the points mode depending on the current acquisiton mode: + acq_type = str(self.instrument.query(":ACQuire:TYPE?")).strip("\n") + if acq_type == "AVER" or acq_type == "HRES": + points_mode = "NORMal" + # Use for Average and High Resoultion acquisition Types. + # If the :WAVeform:POINts:MODE is RAW, and the Acquisition Type is Average, the number of points available is 0. If :WAVeform:POINts:MODE is MAX, it may or may not return 0 points. + # If the :WAVeform:POINts:MODE is RAW, and the Acquisition Type is High Resolution, then the effect is (mostly) the same as if the Acq. Type was Normal (no box-car averaging). + # Note: if you use :SINGle to acquire the waveform in AVERage Acq. Type, no average is performed, and RAW works. + else: + points_mode = "RAW" # Use for Acq. Type NORMal or PEAK + + # This command sets the points mode to MAX AND ensures that the maximum # of points to be transferred is set, though they must still be on screen + self.instrument.write(":WAVeform:POINts MAX") + # The above command sets the points mode to MAX. So we set it here to make sure its what we want. + self.instrument.write(":WAVeform:POINts:MODE " + str(points_mode)) # Check if there is actually data to acquire: - # This line also makes the channel the source for the data export! - data_available = int( - self.query(":WAVeform:SOURce CHANnel" + str(signal) + ";POINTs?") - ) + data_available = int(self.query(":WAVeform:POINTs?")) if data_available == 0: # No data is available # Setting a channel to be a waveform source turns it on, so we need to turn it off now: @@ -678,40 +710,59 @@ def waveform_values(self, signal, file_name="", file_type="csv"): raise ValueError("No data is available") preamble = self.waveform_preamble() - data = self.retrieve_waveform_data() - time_values = [] - values = [] - for index, datum in enumerate(data): - time_val = index * preamble["x_increment"] + preamble["x_origin"] - y_val = ( - preamble["y_origin"] - + (datum - preamble["y_reference"]) * preamble["y_increment"] - ) - time_values.append(time_val) - values.append(y_val) - if file_name and file_type == "csv": # Needs work for multiple references + # Grab the data from the scope: + # "h" datatype should be 2 bytes as defined in the struct module. + # The scope being in "WORD" format should return two bytes per value. + data = self.instrument.query_binary_values( + ":WAV:DATA?", datatype="b", is_big_endian=True + ) + + x = [] + y = [] + # Modify some things if we are in peak detect mode: + data_len = int(len(data) / 2) if acq_type == "PEAK" else len(data) + multiplier = 2 if acq_type == "PEAK" else 1 + for i in range(data_len): + x_val = (i - preamble["x_reference"]) * preamble["x_increment"] + preamble[ + "x_origin" + ] + + if acq_type == "PEAK": + # We need to double up on the time index + # In peak detect mode, the points come out as low(t1),high(t1),low(t2),high(t2) + y_min = ( + preamble["y_origin"] + + (data[i * multiplier] - preamble["y_reference"]) + * preamble["y_increment"] + ) + y_max = ( + preamble["y_origin"] + + (data[i * multiplier + 1] - preamble["y_reference"]) + * preamble["y_increment"] + ) + x.append(x_val) + x.append(x_val) + y.append(y_min) + y.append(y_max) + + else: + y_val = ( + preamble["y_origin"] + + (data[i] - preamble["y_reference"]) * preamble["y_increment"] + ) + + x.append(x_val) + y.append(y_val) + + if file_name and file_type == "csv": with open(file_name, "w") as f: - f.write("x,y") - f.write("\n") - for time_val, y_val in zip(time_values, values): - f.write( - "{time_val},{voltage}\n".format( - time_val=time_val, voltage=y_val - ) - ) + f.write("x,y\n") + for x_val, y_val in zip(x, y): + f.write(f"{x_val},{y_val}") + elif file_name and file_type == "bin": raise NotImplementedError("Binary Output not implemented") - return time_values, values - - def retrieve_waveform_data(self): - self.instrument.write(":WAV:DATA?") - time.sleep(0.2) - data = self.read_raw()[:-1] # Strip \n - if data[0:1] != "#".encode(): - raise InstrumentError("Pound Character missing in waveform data response") - valid_bytes = data[int(data[1:2]) + 2 :] # data[1] denotes length value digits - values = struct.unpack("%dB" % len(valid_bytes), valid_bytes) - return values + return x, y def digitize(self, signals): signals = [self.validate_signal(sig) for sig in signals] From 8567aaff510452a748ea9b80e36be7ad4fe7b3a5 Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Wed, 4 Mar 2026 12:39:55 +1100 Subject: [PATCH 7/8] Update release notes. Update docstring for waveform_values function. --- docs/release-notes.rst | 4 ++-- src/fixate/drivers/dso/agilent_mso_x.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index e41a756f..40f4180d 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,8 +20,8 @@ Improvements - Sequencer logic now handles exceptions raised on sequence abort. GUI will no longer hang when a test raises an exception during a test abort. - Fix bug where DSOX1202G appeared to hang both the program and scope - LCR Driver now supports instruments reporting as Keysight or Agilent. Newer models of the LCR meter report as Keysight, whereas older models report as Agilent. -- DSO Driver function 'waveform_value' now returns a single channels x and y data as two separate lists, without re-acquiring the signal. This function should - now be called after performing singal acquisition. +- DSO Driver function 'waveform_values' now returns a single channels x and y data as two separate lists, without re-acquiring the signal. This function should + now be called after performing signal acquisition. ************* Version 0.6.4 diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index 1cb86c90..a1181699 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -645,7 +645,8 @@ def waveform_preamble(self): def waveform_values(self, signal, file_name="", file_type="csv"): """ - Retrieves waveform data from the specified channel and optionally saves it to a file. + Retrieves currently present waveform data from the specified channel and optionally saves it to a file. + The oscilliscope must be in the stopped state to retrive waveform data. This method queries the instrument for raw data points, scales them using the waveform preamble (origin, increment, and reference values), and From 11048188ea2043b7215a1ae5e935a844cc0177ed Mon Sep 17 00:00:00 2001 From: Jasper-Harvey0 Date: Fri, 6 Mar 2026 07:06:19 +1100 Subject: [PATCH 8/8] Clean up some variables. Remove unused import. --- src/fixate/drivers/dso/agilent_mso_x.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/fixate/drivers/dso/agilent_mso_x.py b/src/fixate/drivers/dso/agilent_mso_x.py index a1181699..7de7d04e 100644 --- a/src/fixate/drivers/dso/agilent_mso_x.py +++ b/src/fixate/drivers/dso/agilent_mso_x.py @@ -1,4 +1,3 @@ -import struct import pyvisa from fixate.core.exceptions import InstrumentError from fixate.drivers.dso.helper import DSO @@ -669,19 +668,19 @@ def waveform_values(self, signal, file_name="", file_type="csv"): try: # If the channel is not able to be converted to an int, then its almost definitely not an analogue source # i.e. you might have requested "math" or "function" that is not supported by this method. - int(signal) + int_signal = int(signal) except ValueError: raise ValueError( "Please select an analog channel. Math or function channels are not supported." ) # Exit early if the requested channel is not currently displayed: - ch_state = int(self.instrument.query(":CHANnel" + str(signal) + ":DISPlay?")) + ch_state = int(self.instrument.query(f":CHANnel{int_signal}:DISPlay?")) if not ch_state: raise ValueError("Requested channel is not active!") # Set the channel: - self.instrument.write(":WAVeform:SOURce CHANnel" + str(signal)) + self.instrument.write(f":WAVeform:SOURce CHANnel{int_signal}") # Explicitly set this to avoid confusion self.instrument.write(":WAVeform:FORMat BYTE") self.instrument.write(":WAVeform:UNSigned 0") @@ -700,20 +699,19 @@ def waveform_values(self, signal, file_name="", file_type="csv"): # This command sets the points mode to MAX AND ensures that the maximum # of points to be transferred is set, though they must still be on screen self.instrument.write(":WAVeform:POINts MAX") # The above command sets the points mode to MAX. So we set it here to make sure its what we want. - self.instrument.write(":WAVeform:POINts:MODE " + str(points_mode)) + self.instrument.write(":WAVeform:POINts:MODE " + points_mode) # Check if there is actually data to acquire: data_available = int(self.query(":WAVeform:POINTs?")) if data_available == 0: # No data is available # Setting a channel to be a waveform source turns it on, so we need to turn it off now: - self.write(":CHANnel" + str(signal) + ":DISPlay OFF") + self.write(f":CHANnel{int_signal}:DISPlay OFF") raise ValueError("No data is available") preamble = self.waveform_preamble() # Grab the data from the scope: - # "h" datatype should be 2 bytes as defined in the struct module. - # The scope being in "WORD" format should return two bytes per value. + # datatype definition is "b" for byte. See struct module details. data = self.instrument.query_binary_values( ":WAV:DATA?", datatype="b", is_big_endian=True )