diff --git a/.gitignore b/.gitignore
index f98d02aa85401bc0d782c52d341d614609c45198..a1cdda760b0429734357b4670254935dc2dfceb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,7 @@
 *.csv
 *graph.pdf
 *analysis.pdf
+Python_script/measurements/*
+Python_script/PostPlots/*
 __pycache__
 
diff --git a/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf b/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..ddcde3e944607597f55a775ddbd95236c08fc839
Binary files /dev/null and b/Python_script/Equipment_Manuals/VNA/ZNA_user_manual.pdf differ
diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..ccde5bfc22e556776805c4677b08ce1c4e4a8c53
Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/cross_sectional_manual_almemo.pdf differ
diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..5bec921c00647a76082f02ad28f64ade4d6da561
Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D6_sensors.pdf differ
diff --git a/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..279ca7cd0cf3a7fd93f1681f05f2d405430b315f
Binary files /dev/null and b/Python_script/Equipment_Manuals/datalogger_env_sensors/operating_instruction_D7_sensors.pdf differ
diff --git a/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..c5873e8bbf907090e49924ae0e40f2df602d7d3b
Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/instrument_manual_PR3J.pdf differ
diff --git a/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..ca4552ae7e188e2db819086995464e1e6b2c5ab0
Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/remote_control__manual_PR3J.pdf differ
diff --git a/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf b/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..46f3e9bfa7533b63831287b30a99b6bcab257eeb
Binary files /dev/null and b/Python_script/Equipment_Manuals/env_test_chamber/user_manual_PR3J.pdf differ
diff --git a/Python_script/MeasurementPlot.py b/Python_script/MeasurementPlot.py
index 5e72951d41eba35fd6d9ac60f3255804bbd10745..e61a11be646c48cbc4b5af34a47cfeb5b3c16dea 100644
--- a/Python_script/MeasurementPlot.py
+++ b/Python_script/MeasurementPlot.py
@@ -3,6 +3,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 import time
 import queue
+import sys
 
 # Different exceptions can be thrown while plotting, depending on the backend.
 # We catch them all locally and raise our own exception instead
@@ -11,48 +12,137 @@ class PlottingError(Exception):
     pass
 
 class MeasurementPlot:
-    def __init__(self, title=''):
-        self.fig, self.ax1 = plt.subplots(2, figsize=(12, 10))
+    def __init__(self, reference_signal_names, title='', trace_subplot5 ='', legend_loc = 'upper left',\
+                 legend_bbox_to_anchor = (1.09, 1)):
+        self.reference_signal_names = reference_signal_names
+
+        # set python for opening an separate plot window when starting from anaconda
+        if 'ipykernel' in sys.modules:
+            from IPython import get_ipython
+            get_ipython().run_line_magic('matplotlib', 'qt')
+        
+        self.trace_subplot5 =  trace_subplot5
+        
+        # parameter for legend of subplots
+        self.legend_loc = legend_loc
+        self.legend_bbox_to_anchor = legend_bbox_to_anchor
+      
+        # Prepare for five subplots
+        self.fig, self.ax1 = plt.subplots(5, figsize=(25, 20))
+        self.fig.subplots_adjust(bottom= 0.1, right=0.8, hspace = 0.4)
         self.fig.suptitle("Measurement "+title, color="red")
 
-        # First plot: Phase and magnitude
-        self.path_collection_phase = self.ax1[0].scatter([], [], c='red', marker='<', label='Phase')
-        self.magnitude_axis = self.ax1[0].twinx()
-        self.path_collection_mag = self.magnitude_axis.scatter([], [], c='#3120E0', marker='4', label='Magnitude')
+        # First plot: signal0 and signal1
+        self.path_collection_signal0 = self.ax1[0].scatter([], [], c='red', marker='<', label=reference_signal_names[0])
+        self.path_collection_fit = self.ax1[0].scatter([], [], c='green', marker='.', label = ' ')
+        self.signal1_axis = self.ax1[0].twinx()
+        
+        self.path_collection_signal1 = self.signal1_axis.scatter([], [], c='#3120E0', marker='4', label=reference_signal_names[1])
         self.equi_axis0 = self.ax1[0].twinx()
-        self.equi_axis0.spines['right'].set_position(('outward', 40))
+        self.equi_axis0.spines['right'].set_position(('outward', 75))
         self.path_collection_equi0 = self.equi_axis0.scatter([], [], c='black', marker=".", label='Equilibrium_Indicator')
 
         self.ax1[0].set_xlabel("TIMESTAMP")
-        self.ax1[0].set_ylabel("PHASE", color='red')
-        self.magnitude_axis.set_ylabel("MAGNITUDE", color='#3120E0')
-        self.equi_axis0.set_ylabel("EQUILIBRIUM_INDICATOR", color='black')
+        self.ax1[0].set_ylabel(reference_signal_names[0], color='red')
+        self.signal1_axis.set_ylabel(reference_signal_names[1], color='#3120E0')
+        self.equi_axis0.set_ylabel("INDICATOR VALUE", color='black')
         # fix range to 0..31 with some extra margin for plotting
         self.equi_axis0.set_ylim(-1, 32)
 
         self.ax1[0].grid(True, linestyle=":")
-        all_path_collections = [self.path_collection_phase, self.path_collection_mag, self.path_collection_equi0]
+        all_path_collections = [self.path_collection_signal0, self.path_collection_signal1,
+                                self.path_collection_equi0, self.path_collection_fit]
         labels = [pc.get_label() for pc in all_path_collections]
-        self.ax1[0].legend(all_path_collections, labels, loc='lower right')
+        self.signal0_legend = self.ax1[0].legend(all_path_collections, labels, loc=self.legend_loc,
+                                               bbox_to_anchor=self.legend_bbox_to_anchor)
+            
+        ax = self.fig.axes
+        self.annotation = ax[0].annotate('', xy=(0, 1),
+                                         xycoords='axes fraction', xytext=(-0.16, 1),
+                                         textcoords='axes fraction', fontsize='16',
+                                         horizontalalignment='left', verticalalignment='bottom')
 
-        # Second plot: Humidity and temperature
-        self.path_collection_temp = self.ax1[1].scatter([], [], c='blue', marker='p', label="Temperature")
+        # Second plot: Humidity and temperature of climate chamber requested from internal sensors of chamber
+        self.path_collection_temp = self.ax1[1].scatter([], [], c='blue', marker='p', label="Chamber Temperature")
         self.humidity_axis = self.ax1[1].twinx()
-        self.path_collection_hum = self.humidity_axis.scatter([], [], c='green', marker="*", label="Humidity")
+        self.path_collection_hum = self.humidity_axis.scatter([], [], c='green', marker="*", label="Chamber Humidity")
         self.equi_axis1 = self.ax1[1].twinx()
-        self.equi_axis1.spines['right'].set_position(('outward', 40))
-        self.path_collection_equi1 = self.equi_axis1.scatter([], [], c='black', marker=".", label="Equilibrium_Indicator")
+        self.equi_axis1.spines['right'].set_position(('outward', 75))
+        self.path_collection_equi1 = self.equi_axis1.scatter([], [], c='black', marker=".",
+                                                             label="Equilibrium Indicator")
 
         self.ax1[1].set_xlabel("TIMESTAMP")
-        self.ax1[1].set_ylabel("TEMPERATURE ", color='blue')
-        self.humidity_axis.set_ylabel("HUMIDITY", color='green')
-        self.equi_axis1.set_ylabel("EQUILIBRIUM_INDICATOR", color='black')
+        self.ax1[1].set_ylabel("TEMPERATURE [°C] ", color='blue')
+        self.humidity_axis.set_ylabel("HUMIDITY [%RH]", color='green')
+        self.equi_axis1.set_ylabel("INDICATOR VALUE", color='black')
         self.equi_axis1.set_ylim(-1, 32)
 
         self.ax1[1].grid(True, linestyle=":")
         all_path_collections = [self.path_collection_temp, self.path_collection_hum, self.path_collection_equi1]
         labels = [pc.get_label() for pc in all_path_collections]
-        self.ax1[1].legend(all_path_collections, labels, loc='lower right')
+        self.ax1[1].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor)
+
+        # Third plot:  parameter of external sensors DUT temperature,  DUT humidity
+        self.path_collection_temp_dut = self.ax1[2].scatter([], [], c='red', marker='p', label='DUT temperature')
+        
+        self.ext_sens_hum_axis = self.ax1[2].twinx()
+        self.path_collection_hum_dut = self.ext_sens_hum_axis.scatter([], [], c='purple', marker='*',
+                                                                      label='DUT humidity')
+        
+        
+        self.ax1[2].set_xlabel("TIMESTAMP")
+        self.ax1[2].set_ylabel("TEMPERATURE [°C]", color='red')
+        self.ext_sens_hum_axis.set_ylabel("HUMIDITY [%RH]", color = 'purple')
+
+        self.ax1[2].grid(True, linestyle=":")
+        all_path_collections = [self.path_collection_temp_dut, self.path_collection_hum_dut]
+        labels = [pc.get_label() for pc in all_path_collections]
+        self.ax1[2].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor)
+        
+        # Forth plot: parameter of external sensors: room temperature,  room humidity , air pressure room
+        self.path_collection_temp_room = self.ax1[3].scatter([], [], c='green', marker='*', label='room temperature')
+        
+        self.sec_ext_hum_sens_axis = self.ax1[3].twinx()
+        self.path_collection_hum_room = self.sec_ext_hum_sens_axis.scatter([], [], c='orange', marker='>',
+                                                                           label='room humidity')
+        
+        self.press_axis = self.ax1[3].twinx()
+        self.press_axis.spines['right'].set_position(('outward', 60))
+        self.path_collection_air_press_room = self.press_axis.scatter([], [], c='grey', marker='4',
+                                                                      label='air pressure room')
+        
+        self.ax1[3].set_xlabel("TIMESTAMP")
+        self.ax1[3].set_ylabel("TEMPERATURE [°C]", color='green')
+        self.sec_ext_hum_sens_axis.set_ylabel("HUMIDITY [%RH]", color='orange')
+        self.press_axis.set_ylabel("AIR PRESSURE [mb]", color='grey')
+        
+        self.ax1[3].grid(True, linestyle=":")
+        all_path_collections = [self.path_collection_temp_room, self.path_collection_hum_room,
+                                self.path_collection_air_press_room]
+        labels = [pc.get_label() for pc in all_path_collections]
+        self.ax1[3].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor)
+                
+        # Fifth plot: parameter of external sensors: meas instruments temperature,  meas instruments humidity
+        subplot_dict = self.config_fifth_subplot()
+        
+        self.path_collection_trace_1 = self.ax1[4].scatter([], [], c='black', marker='p',
+                                                           label=subplot_dict.get('label_trace_1'))
+        
+        self.sec_plot_param_axis = self.ax1[4].twinx()
+        self.path_collection_trace_2 = self.sec_plot_param_axis.scatter([], [], c='brown', marker="*",
+                                                                        label=subplot_dict.get('label_trace_2'))
+        
+        self.ax1[4].set_xlabel("TIMESTAMP")
+        self.ax1[4].set_ylabel(subplot_dict.get('y_axis'), color='black')
+        self.sec_plot_param_axis.set_ylabel(subplot_dict.get('sec_y_axis'), color='brown')
+        
+        self.ax1[4].grid(True, linestyle=":")
+        all_path_collections = [self.path_collection_trace_1, self.path_collection_trace_2]
+            
+        labels = [pc.get_label() for pc in all_path_collections]
+        self.ax1[4].legend(all_path_collections, labels, loc=self.legend_loc, bbox_to_anchor=self.legend_bbox_to_anchor)
+
+        plt.rcParams.update({'font.size': 16})
 
         self.data_queue = queue.Queue(10)
 
@@ -80,26 +170,76 @@ class MeasurementPlot:
         minimum, maximum = self.get_extended_min_max(timestamps)
         self.ax1[0].set_xlim(minimum, maximum)
         self.ax1[1].set_xlim(minimum, maximum)
+        self.ax1[2].set_xlim(minimum, maximum)
+        self.ax1[3].set_xlim(minimum, maximum)
+        self.ax1[4].set_xlim(minimum, maximum)
 
-        phases = data_frame.S21_PHASE
-        minimum, maximum = self.get_extended_min_max(phases)
+        # refresh data for signal0 in subplot for signal0 and signal1
+        signal0 = data_frame[self.reference_signal_names[0]]
+        minimum, maximum = self.get_extended_min_max(signal0)
         self.ax1[0].set_ylim(minimum, maximum)
-        self.path_collection_phase.set_offsets(np.c_[timestamps, phases])
+        self.path_collection_signal0.set_offsets(np.c_[timestamps, signal0])
+        self.path_collection_fit.set_offsets(np.c_[[], []])
 
-        magnitudes = data_frame.S21_MAGNITUDE
-        minimum, maximum = self.get_extended_min_max(magnitudes)
-        self.magnitude_axis.set_ylim(minimum, maximum)
-        self.path_collection_mag.set_offsets(np.c_[timestamps, magnitudes])
+        # refresh data for signal1 in subplot for signal0 and signal1
+        signal1s = data_frame[self.reference_signal_names[1]]
+        minimum, maximum = self.get_extended_min_max(signal1s)
+        self.signal1_axis.set_ylim(minimum, maximum)
+        self.path_collection_signal1.set_offsets(np.c_[timestamps, signal1s])
 
+        # refresh data for chamber temperature in subplot for chamber temperature and humidity  
         temperatures = data_frame.READBACK_TEMPERATURE
         minimum, maximum = self.get_extended_min_max(temperatures)
         self.ax1[1].set_ylim(minimum, maximum)
         self.path_collection_temp.set_offsets(np.c_[timestamps, temperatures])
 
+        # refresh data for chamber humidity in subplot for chamber temperature and humidity
         humidities = data_frame.READBACK_HUMIDITY
         minimum, maximum = self.get_extended_min_max(humidities)
         self.humidity_axis.set_ylim(minimum, maximum)
         self.path_collection_hum.set_offsets(np.c_[timestamps, humidities])
+        
+        # refresh temperatures for used external sensors in subplots
+        temp_dut = data_frame.TEMP_DUT
+        temp_room = data_frame.TEMP_ROOM
+        
+        minimum, maximum = self.get_extended_min_max(temp_dut)
+        self.ax1[2].set_ylim(minimum, maximum)
+        self.path_collection_temp_dut.set_offsets(np.c_[timestamps, temp_dut])
+        
+        minimum, maximum = self.get_extended_min_max(temp_room)
+        self.ax1[3].set_ylim(minimum, maximum)
+        self.path_collection_temp_room.set_offsets(np.c_[timestamps, temp_room])
+        
+        # refresh humidities external sensors in subplots for DUT humidity and room humidity
+        hum_dut = data_frame.HUM_DUT
+        hum_room = data_frame.HUM_ROOM
+
+        minimum, maximum = self.get_extended_min_max(hum_dut)
+        self.ext_sens_hum_axis.set_ylim(minimum, maximum)
+        
+        minimum, maximum = self.get_extended_min_max(hum_room)
+        self.sec_ext_hum_sens_axis.set_ylim(minimum, maximum)
+        self.path_collection_hum_dut.set_offsets(np.c_[timestamps, hum_dut])
+        self.path_collection_hum_room.set_offsets(np.c_[timestamps, hum_room])
+        
+        # refresh air pressure of external sensor in subplot for air pressure room
+        air_press_room = data_frame.AIR_PRESS_ROOM
+        minimum, maximum = self.get_extended_min_max(air_press_room)
+        self.press_axis.set_ylim(minimum, maximum)
+        self.path_collection_air_press_room.set_offsets(np.c_[timestamps, air_press_room])
+        
+        # refresh temperature and humidity of external sensor in subplot for measurement
+        # for instrument temperature and measurement instrument humidity
+        val_trace_1, val_trace_2 = self.refresh_param_fifth_subplot(data_frame)
+        
+        minimum, maximum = self.get_extended_min_max(val_trace_1)
+        self.ax1[4].set_ylim(minimum, maximum)
+
+        minimum, maximum = self.get_extended_min_max(val_trace_2)               
+        self.sec_plot_param_axis.set_ylim(minimum, maximum)
+        self.path_collection_trace_1.set_offsets(np.c_[timestamps, val_trace_1])
+        self.path_collection_trace_2.set_offsets(np.c_[timestamps, val_trace_2])
 
         self.path_collection_equi0.set_offsets(np.c_[timestamps, data_frame.EQUILIBRIUM_INDICATOR])
         self.path_collection_equi1.set_offsets(np.c_[timestamps, data_frame.EQUILIBRIUM_INDICATOR])
@@ -114,21 +254,78 @@ class MeasurementPlot:
                 self.fig.canvas.flush_events()
             except Exception as e:
                 raise PlottingError from e
+    
+    def config_fifth_subplot(self):
+        
+        # key names for config parameter fifth subplot
+        keys = ["label_trace_1", "label_trace_2", "y_axis", "sec_y_axis"]
+        
+        if self.trace_subplot5 == 'logger_sens':
+            
+            # values for plotting evnironmental conditions in measurement instrument chamber
+            values = ["temperature\nalmemo sensors\nmeas instr chamber",
+                      "humidity\nalmemo sensors\nmeas instr chamber",
+                      "TEMPERATURE [°C]", "HUMIDITY [%RH]"]
+        
+        elif self.trace_subplot5 == 'chamber_sens':
+            # values for plotting environmental conditions in measurement instrument chamber
+            values = ["temperature\nchamber sensors\nmeas instr chamber",
+                      "humidity\nchamber sensors\nmeas instr chamber",
+                      "TEMPERATURE [°C]", "HUMIDITY [%RH]"]
+        else:
+            
+            # values for plotting heater activity in fifth subplot
+            values = ["Temp Heater\nDUT chamber", "Humidity Heater\n DUT chamber",
+                      "ACTIVITY [%]","ACTIVITY [%]"]
+
+        # generate dictionary from selection
+        config_subplot_dict = dict(zip(keys, values))
+            
+        return config_subplot_dict
+
+    def refresh_param_fifth_subplot(self, data_frame):
+        
+        if self.trace_subplot5 == 'logger_sens':
+            
+            # chose sensor values for refreshing fifth subplot
+            val_trace_1 =  data_frame.TEMP_MEAS_INSTR      
+            val_trace_2 = data_frame.HUM_MEAS_INSTR
+            
+        elif self.trace_subplot5 == 'chamber_sens': 
+            
+            val_trace_1 =  data_frame.READBACK_TEMP_MEAS_INSTR      
+            val_trace_2 = data_frame.READBACK_HUM_MEAS_INSTR
+        
+        else:
+            
+            # chose heater values for refreshing fifth plot
+            val_trace_1 = data_frame.TEMP_HEATER      
+            val_trace_2 = data_frame.HUM_HEATER
+
+        return val_trace_1, val_trace_2
 
     # add 5 % of the distance between min and max to the range
     @staticmethod
     def get_extended_min_max(array):
         distance = array.max() - array.min()
         if distance == 0.:
-            distance = 0.5
+            distance = 1
         return array.min()-0.05*distance, array.max()+0.05*distance
 
+
+# test procedure for measurement plot procedure
 if __name__ == '__main__':
-    m = MeasurementPlot()
+    # possible selections trace subplot 5
+    # chamber_sens
+    # logger_sens
+    # heater_dut_chamber
+    
+    m = MeasurementPlot(['S21_PHASE', 'S21_MAGNITUDE'], trace_subplot5="chamber_sens")
     plt.ion()
     measurements = []
 
     #FIXME: The loop should run in a separate thread and use draw_in_other_thread
+# generation of datapoints for plot
     for i in range(20):
         measurement = {
             'TIMESTAMP': i,
@@ -136,11 +333,27 @@ if __name__ == '__main__':
             'READBACK_HUMIDITY': 10 + 0.1*i,
             'EQUILIBRIUM_INDICATOR': i % 4,
             'S21_PHASE': 20 - 2*i,
-            'S21_MAGNITUDE': 0.3*i
-        }
+            'S21_MAGNITUDE': 0.3*i,
+       
+            'TEMP_HEATER': 10,
+            'HUM_HEATER': 3,
+            'TEMP_DUT': i,
+            'TEMP_ROOM': 25-i,
+            'HUM_DUT': 40,
+            'HUM_ROOM': 45,
+            'AIR_PRESS_ROOM': 1000+10*i,
+            'TEMP_MEAS_INSTR': 40-1.5*i,
+            'HUM_MEAS_INSTR': 55,
+            
+            'READBACK_TEMP_MEAS_INSTR': 50-2*i,
+            'READBACK_HUM_MEAS_INSTR': 39
+            }
+        
         measurements.append(measurement)
         my_data_frame = pd.DataFrame(measurements)
+        # plot of data frame with test data for actual step
         m.draw_in_this_thread(my_data_frame)
+        # plot of step number
         print(str(i))
         time.sleep(0.3)
 
diff --git a/Python_script/PostPlot.py b/Python_script/PostPlot.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ff91d6bb8f4bf94eed4ecc0f933c2adc954a27d
--- /dev/null
+++ b/Python_script/PostPlot.py
@@ -0,0 +1,191 @@
+import pandas as pd
+import sys
+from pathlib import Path
+import os
+from MeasurementPlot import MeasurementPlot
+import numpy as np
+
+class PostPlot:
+    """
+    Generate a plot of all data files from the individual measurements to have one plot of the whole
+    data taking sequence.
+    Optionally, correlation and regression can be calculated and plotted.
+    """
+    def __init__(self, reference_signal_names, trace_subplot5='', legend_loc='upper left', legend_bbox_to_anchor=(1.09, 1)):
+        # set python for opening an separate plot window
+        if 'ipykernel' in sys.modules:
+            from IPython import get_ipython
+            get_ipython().run_line_magic('matplotlib', 'qt')
+        
+        self.measplot = MeasurementPlot(reference_signal_names, trace_subplot5=trace_subplot5,
+                                        legend_loc=legend_loc, legend_bbox_to_anchor=legend_bbox_to_anchor)
+        
+        self.legend_loc = legend_loc
+        self.legend_bbox_to_anchor = legend_bbox_to_anchor
+        
+        # set parameter figure of class to object parameter figure
+        self.fig = self.measplot.fig
+
+        self.reference_signal_names = reference_signal_names
+    
+    # read csv-file and import data to data frame
+    def import_csv(self, csv_file):
+        
+        data_frame = pd.read_csv(csv_file)
+        
+        return data_frame
+                
+    def plot_frame_data(self, data_frame, title, time_unit ='min', measurement_set=None, ):
+        
+        # set title of plot 
+        self.fig.suptitle("Measurement "+title, color="red")
+        
+        # reset index of data frame
+        data_frame.reset_index(inplace=True, drop=True)
+        
+        # set y-Achlabel in all subplots to Time an in square brackets the selected time unit 
+        for element in self.measplot.ax1:
+            element.set_xlabel("Time [%s]" %time_unit)         
+        
+        # make a copy of data_frame in parameter list without changing original during modification
+        if measurement_set is None:
+            postplot_data_frame = data_frame.copy()
+        else:
+            postplot_data_frame = data_frame.loc[data_frame['SET_NAME'] == measurement_set].copy().reset_index()
+
+        # time stamp of index = 0 is the start point of time axis
+        time_sec = postplot_data_frame.TIMESTAMP - postplot_data_frame.TIMESTAMP[0]
+        
+        # Set scaling of time axis depending on the time unit given in parameter list.
+        # Default unit is minutes
+        time_vals = self.scaling_time_axes(time_sec, time_unit)
+        
+        # update Timestamps with calculated time values         
+        postplot_data_frame.update(time_vals) 
+        
+        # refresh subplots with data in data frame
+        self.measplot.draw(postplot_data_frame, pdf_name='')
+        
+        # cal PK2PK values of magnitude and phase 
+        PK2PK = self.calc_pkpk_values(postplot_data_frame)
+
+        self.edit_annotation_in_plot(annotate_string=PK2PK)
+        
+    def edit_annotation_in_plot(self, annotate_string='', anno_fontsize=16):
+        
+        self.measplot.annotation.set_text(annotate_string)
+        self.measplot.annotation.set_fontsize(anno_fontsize)
+
+    def calc_pkpk_values(self, data_frame):
+        
+        # calc PK2PK values of the two reference signals
+        delta0 = max(data_frame[self.reference_signal_names[0]]) - min(data_frame[self.reference_signal_names[0]])
+        delta1 = max(data_frame[self.reference_signal_names[1]]) - min(data_frame[self.reference_signal_names[1]])
+        
+        # generate text for annotation in first subplot
+        # FIXME: This used to contain units. We will need some 'pretty printing' values for the nanes
+        delta_vals_string = ('$\Delta_{PkPk}$'+self.reference_signal_names[0]+': '+str(delta0)+'\n$\Delta_{PkPk}$'+
+                             self.reference_signal_names[1]+': '+str(delta1))
+            
+        return delta_vals_string
+       
+    def add_curvefit_to_plot(self, y_lim=None, xvals=[], yvals=[], trace_color='None', trace_label=''):
+        if y_lim is not None:
+            # set axis for phase plot to min, max values
+            self.measplot.ax1[0].set_ylim(y_lim)
+            
+        self.measplot.path_collection_fit.set_color(trace_color)
+        self.measplot.path_collection_fit.set_label(trace_label)
+
+        # refresh data in meas plot
+        self.measplot.path_collection_fit.set_offsets(np.c_[xvals, yvals])
+        
+        # get legend handles and labels y-axes from subplot magnitude, phase
+        handles_phase, labels_phase = self.measplot.ax1[0].get_legend_handles_labels()
+        handles_mag, labels_mag = self.measplot.magnitude_axis.get_legend_handles_labels()
+        handles_equi0, labels_eqi0 = self.measplot.equi_axis0.get_legend_handles_labels()
+            
+        handles = handles_phase + handles_mag + handles_equi0
+        labels = labels_phase + labels_mag + labels_eqi0
+        
+        # update legend subplot phase, magnitude
+        self.measplot.ax1[0].legend(handles, labels, loc=self.legend_loc,
+                                    bbox_to_anchor=self.legend_bbox_to_anchor)
+        
+        # refresh plot window
+        self.measplot.fig.canvas.flush_events()
+    
+    # save figure under the given path and file name
+    def save_fig(self, storepath, filename):
+        if not os.path.exists(storepath):
+            os.makedirs(storepath)
+        
+        self.fig.savefig(os.path.join(storepath, filename))
+    
+    # scaling time axes depending on the time unit
+    def scaling_time_axes(self, time_sec, time_unit):
+        if time_unit == 'min':
+            time_vals = time_sec/60
+        
+        elif time_unit == 'hours':
+            time_vals = time_sec/3600
+        
+        elif time_unit == 'sec':
+            time_vals = time_sec
+            
+        else:
+            time_vals = time_sec/60
+    
+        return time_vals
+    
+
+if __name__ == '__main__':
+    
+    
+    # set result path for post plot 
+    Results_Path = r'TestData_JBY240'
+    
+    time_unit = 'min'
+    
+    storepath = os.path.join(Results_Path, 'PostPlots')
+    
+    # search all csv files in results folder 
+    csv_file_list = list(Path(Results_Path).glob("**/*.csv"))
+    
+    # selection measurment data should be plotted in subplot5
+    # 'logger_sens' : values for temperature and humidity of logger sensor in
+    # measurement instrument chmaber
+    # 'chamber_sens' : values for temepratere and humidity readback from chamber sensors
+    # of chamber for measurement instruments
+    # 'heater_dut_chamber': activity of temp heater and hum heater readback from DUT chamber
+    # This is also the default parameter
+    
+    
+    trace_selection = ""
+    
+    plot_obj = PostPlot(trace_subplot5=trace_selection)
+    
+    # empty data frame for concat the data frames from csv import to plot full transition
+    concat_data_frame = pd.DataFrame()
+    
+    # plot results for each csv-file
+    for index, csv_file in enumerate(csv_file_list):
+    
+        data_frame = plot_obj.import_csv(str(csv_file))
+        title = csv_file.name
+        
+        # concatenate data frames for plotting full transition data
+        concat_data_frame = pd.concat([concat_data_frame, data_frame], ignore_index=True, sort=False)
+
+        plot_obj.plot_frame_data(data_frame, title, time_unit)
+
+        filename = str(csv_file.stem) + '.pdf'
+        plot_obj.save_fig(storepath, filename)
+    
+    # plot of all steps of a sweep is plotted in one diagram
+    plot_obj.plot_frame_data(concat_data_frame, 'Full Transition', time_unit)
+    
+    filename = 'Full_Transition' + '.pdf'
+    plot_obj.save_fig(storepath, filename)
+
+  
\ No newline at end of file
diff --git a/Python_script/SoftwareVersion.py b/Python_script/SoftwareVersion.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4d870191513050624d7a25245df7af772bfa2ce
--- /dev/null
+++ b/Python_script/SoftwareVersion.py
@@ -0,0 +1,5 @@
+import subprocess
+
+def software_version():
+    return subprocess.check_output(['git', 'describe']).strip().decode()
+
diff --git a/Python_script/TestStandGUI.bat b/Python_script/TestStandGUI.bat
new file mode 100644
index 0000000000000000000000000000000000000000..425416afafb83fa13a5a85efd95963111f827391
--- /dev/null
+++ b/Python_script/TestStandGUI.bat
@@ -0,0 +1,6 @@
+@echo off
+cd..
+cd..
+cd C:\git\climate-lab-test-stand\Python_script & REM path has to be changed to folder with file "climate-lab-gui.py"
+python C:\git\climate-lab-test-stand\Python_script\climate-lab-gui.py %* & REM Python_script & REM path has to be changed to folder with file "climate-lab-gui.py"
+pause
\ No newline at end of file
diff --git a/Python_script/VNA.py b/Python_script/VNA.py
index aa0f37d93f3589c15bfe6c2b0009d3005c12e47b..0e39b7a8c7813cca19b1a95a591e2f20ea4a519a 100755
--- a/Python_script/VNA.py
+++ b/Python_script/VNA.py
@@ -17,6 +17,8 @@ class Vna:
                 Implements the basic operation of the VNA
 
         """
+        self.vna = None
+
         # open connection here
 
         # store the credentials so that methods can access them
@@ -196,9 +198,7 @@ class Vna:
         self.vna.write("SYST:PRES; *OPC?")
         self.vna.read()
 
-    #Hack: put in the frequency which is configured in the config file to guess if loading was
-    #successful. FIXME: Do proper error checking here
-    def load_config(self, config_file, frequency):
+    def load_config(self, config_file):
         """
         Load the config from the VNA's internal drive. It must be in the folder
         C:\\Users\\Public\\Documents\\Rohde-Schwarz\\ZNA\\RecallSets\\
@@ -230,8 +230,13 @@ class Vna:
         #wait until finished
         self.vna.query('*OPC?')
         
+    # reset status registers and queues of the VNA
+    def reset_status(self):
+        self.vna.write('*CLS')
+    
     def close(self):
-        self.vna.close()
+        if self.vna is not None:
+            self.vna.close()
 
     def __del__(self):
         self.close()
diff --git a/Python_script/VNA_dummy.py b/Python_script/VNA_dummy.py
index 69260881b8b9cf1149e6f02f12ea365906f370a3..1429a036ba518a236481669bce39d9b9f16c5a95 100644
--- a/Python_script/VNA_dummy.py
+++ b/Python_script/VNA_dummy.py
@@ -23,17 +23,17 @@ class VnaDummy:
         self.simulated_traces = [('Trc1', 's11')]
         self.result_format = 'SDAT'
         self.simulated_power = '10.dBm'
-        self.simulated_frequency = '10.MHz'
+        self.simulated_frequency = 1000000
         self.simulated_magnitude_difference_temp = 0.0
         self.simulated_phase_difference_temp = 0.0
         self.simulated_magnitude_difference_hum = 0.0
         self.simulated_phase_difference_hum = 0.0
         self.simulated_state = shared_simulated_state.get_simulated_state()
         self.last_simulated_time = self.simulated_state.simulated_time
-        self.tau_mag_temp = 25     # change rate of magnitude in dependence on temperature
-        self.tau_phase_temp = 35   # change rate of phase in dependence on temperature
-        self.tau_mag_hum = 250     # change rate of magnitude in dependence on humidity
-        self.tau_phase_hum = 350   # change rate of phase in dependence on humidity
+        self.tau_mag_temp = 25  # change rate of magnitude in dependence on temperature
+        self.tau_phase_temp = 35  # change rate of phase in dependence on temperature
+        self.tau_mag_hum = 250  # change rate of magnitude in dependence on humidity
+        self.tau_phase_hum = 350  # change rate of phase in dependence on humidity
         self.reference_magnitude = 0.7  # at reference temp and reference hum
         self.reference_phase = 70  # at reference temp and reference hum
         self.reference_temp = 25
@@ -65,9 +65,8 @@ class VnaDummy:
                                                    delta_magnitude_temp * \
                                                    (1 - (math.exp(-1. * delta_time / self.tau_mag_temp)))
         self.simulated_magnitude_difference_hum = self.simulated_magnitude_difference_hum + \
-                                                   delta_magnitude_hum * \
-                                                   (1 - (math.exp(-1. * delta_time / self.tau_mag_hum)))
-
+                                                  delta_magnitude_hum * \
+                                                  (1 - (math.exp(-1. * delta_time / self.tau_mag_hum)))
 
     def simulate_phase(self):
 
@@ -75,17 +74,17 @@ class VnaDummy:
                                        (self.simulated_state.simulated_temperature - self.reference_temp)
         delta_phase_temp = target_phase_difference_temp - self.simulated_phase_difference_temp
         target_phase_difference_hum = self.phase_slope_hum * \
-                                       (self.simulated_state.simulated_humidity - self.reference_hum)
+                                      (self.simulated_state.simulated_humidity - self.reference_hum)
         delta_phase_hum = target_phase_difference_hum - self.simulated_phase_difference_hum
 
         delta_time = self.simulated_state.simulated_time - self.last_simulated_time
 
         self.simulated_phase_difference_temp = self.simulated_phase_difference_temp + \
-                                               delta_phase_temp *\
+                                               delta_phase_temp * \
                                                (1 - (math.exp(-1. * delta_time / self.tau_phase_temp)))
         self.simulated_phase_difference_hum = self.simulated_phase_difference_hum + \
-                                               delta_phase_hum *\
-                                               (1 - (math.exp(-1. * delta_time / self.tau_phase_hum)))
+                                              delta_phase_hum * \
+                                              (1 - (math.exp(-1. * delta_time / self.tau_phase_hum)))
 
     def get_tupple_of_current_traces(self):
         """
@@ -108,7 +107,9 @@ class VnaDummy:
         self.simulated_power = power
 
     def get_current_cw_frequency(self):
-        return self.simulated_frequency
+        # Fixme: This returns a string, just like the real VNA. Needs to be fixed in both. We keep this for
+        # consistency (although the real thing adapt the unit to orders of magnitude (1.MHz instead of 1000000.Hz
+        return str(self.simulated_frequency) + 'Hz'
 
     def set_cw_frequency(self, frequency):
         self.simulated_frequency = frequency
@@ -123,10 +124,12 @@ class VnaDummy:
             raise Exception('trace ' + trace + ' is not known to VNA dummy!')
         self.result_format = result_format
 
-        simulated_magnitude = self.reference_magnitude + self.simulated_magnitude_difference_temp + \
-            self.simulated_magnitude_difference_hum
-        simulated_phase = self.reference_phase + self.simulated_phase_difference_temp + \
-            self.simulated_phase_difference_hum
+        simulated_magnitude = (self.reference_magnitude +
+                               (self.simulated_magnitude_difference_temp + self.simulated_magnitude_difference_hum)
+                               * 1.3e9 / self.simulated_frequency)
+        simulated_phase = (self.reference_phase +
+                           (self.simulated_phase_difference_temp + self.simulated_phase_difference_hum) *
+                           self.simulated_frequency / 1.3e9)
         phase_radian = simulated_phase / 180. * math.pi
         real_val = simulated_magnitude * math.cos(phase_radian)
         imaginary_val = simulated_magnitude * math.sin(phase_radian)
@@ -148,8 +151,11 @@ class VnaDummy:
                 result.append(float(x))
         return result
 
-    def load_config(self, config_file, frequency):
+    def load_config(self, config_file):
         pass
 
     def do_single_sweep(self):
         pass
+
+    def reset_status(self):
+        pass
diff --git a/Python_script/almemo2490.py b/Python_script/almemo2490.py
new file mode 100644
index 0000000000000000000000000000000000000000..5af34a4fbdea8309dbd68ce296e64c29657c0bac
--- /dev/null
+++ b/Python_script/almemo2490.py
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Jan  2 11:08:12 2023
+
+@author: michael pawelzik
+Some parts have been copied from uros mavric and then they have been modified for compatibility. 
+
+#### SYNTAX FOR CALLING CLASS OBJECTS ########################################
+
+Look into module play_with_almemo2490.py
+
+##############################################################################
+"""
+
+
+# import pythod modules
+import time
+from telnetlib import Telnet
+import re
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import traceback
+from functools import wraps
+
+
+# python class for error handling of almemo 2490
+class err_hdl(Exception):
+    
+    # function for retry decorate object in case of exception
+    def do_retry(ExceptionToCheck = Exception, tries=4, delay=3, backoff=2, \
+                 default_response = None):
+        """Retry calling the decorated function using an backoff and defaults for return
+    
+        original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
+        several lines of code have been modified for functionality
+        
+        :param ExceptionToCheck: the exception to check. may be a tuple of
+            exceptions to check
+        :type ExceptionToCheck: Exception or tuple
+        :param tries: number of times to try (not retry) before giving up
+        :type tries: int
+        :param delay: initial delay between retries in seconds
+        :type delay: int
+        :param backoff: backoff multiplier e.g. value of 2 will double the delay
+            each retry
+        :param default response: default values to return if all tries fail
+        : type default response: Value or tuple
+        
+        """
+        def decorator_retry(function):
+    
+            @wraps(function)
+            def function_retry(*args, **kwargs):
+                
+                mtries, mdelay = tries, delay
+                while mtries > 0:
+                    try:
+                        return function(*args, **kwargs)
+                    except ExceptionToCheck:
+                        print("ALMEO2490: Error occured in %s,'Retrying in %d seconds" %(function.__name__, mdelay))
+                        time.sleep(mdelay)
+                        mtries -= 1
+                        mdelay += backoff
+                raise Exception('maximum number of retries [%d] reached\nfunction: %s\nmodule: %s' \
+                      % (tries, function.__name__,function.__module__)) 
+                
+            return function_retry # true decorator
+            
+        return decorator_retry
+    
+    # function for handle exception
+    def do_handle_exceptions(ExceptionToCheck = Exception, default_response = None):
+        
+        """ error calling the decorated function using defaults for return
+        original from: 
+        https://www.3resource.com/python-excercises/decorator/python-decorator-exercise-9.php
+        
+        several lines of code have been modified for functionality
+        :param ExceptionToCheck: the exception to check. may be a tuple of
+            exceptions to check
+        :type ExceptionToCheck: Exception or tuple    
+        :param default response: default values to return if all tries fail
+        : type default response: Value or tuple
+            
+        """
+        def exception_decorator(function):
+            
+            def function_handle_exception(*args, **kwargs):
+                
+                try:
+                    return function(*args, **kwargs)
+                
+                except ExceptionToCheck as e:
+                    
+                    print('ALMEO2490: ' + str(type(e).__name__) + " occured in:\nfunction: %s\nmodule: %s" \
+                          % (function.__name__,function.__module__))
+                    print(traceback.print_tb(e.__traceback__))
+    
+                    return default_response
+    
+            return function_handle_exception
+        
+        return exception_decorator
+
+
+class almemo2490:
+   
+    
+    # constructor of class
+    def __init__(self, ip = '192.168.115.44', timeout = 10):
+        self.tn = None
+        
+        # set class variabel with ip adress given in parameter list of constructor
+        self.ip_adress = ip 
+        
+        # set class variable with timeout value given in parameter list of constructor
+        self.timeout = timeout
+             
+        # open telenet connection to ALMEMO 2490
+        self.tn = self.open_connection()
+
+        # create data frame for measurment buffer to store parameter and measured
+        # properties of connected sensors to ALMEMO 2490
+        self.meas_buffer = pd.DataFrame(columns = ['meas_date','meas_time', 'sens_port', \
+                                                   'sens_channel', 'meas_val', 'channel_unit', \
+                                                   'channel_name'])
+   
+   
+
+    """
+    ##### commands are echoed. Will cause conflicts in communication and there it is obmitted #### 
+    # write function to send commands to ALMEMO 2490 that do not send a
+    # response
+    def write(self, cmd_str, wait_time = 0.5):
+     
+        # send command to ALMEMO 2490
+        self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r')
+     
+        # wait
+        time.sleep(wait_time)
+    """ 
+    @err_hdl.do_retry(tries=5, delay=1, backoff=3)
+    def open_connection(self):
+        
+        return Telnet(self.ip_adress, 10001, self.timeout)
+        
+    
+    # query function to send a command to almemo 2490 and
+    # return the response to the command
+ 
+    def query(self, cmd_str, wait_time = 1):
+        
+        # send request to ALMEMO 2490 encode UTF8 with CR
+        self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r')
+        
+        # wait
+        time.sleep(wait_time)
+        
+        # read_buffer = self.tn.read_very_eager()
+        # read buffer until END OF TEXT character has been reach or timeout has occured
+        read_buffer = self.tn.read_until(b'\x03',self.timeout)
+        
+        # decode receivved data with CP437 format
+        decoded_read_buffer = read_buffer.decode('cp437')
+        
+        
+        # return received data
+        return decoded_read_buffer
+   
+
+   # method to set date of ALMEMO 2490
+    @err_hdl.do_handle_exceptions()
+    def set_date(self):
+        
+       # request date from operating system
+       # convert requested date to string
+       # date format: ddmmyy (d: day, m: month, y: year, last 2 digits )
+       date = datetime.strftime(datetime.now(),"%d%m%y")
+        
+       # cmd string for setting date of device
+       cmd_str = 'd' + date
+       
+       # send command for setting date to device 
+       self.query(cmd_str)
+       
+   
+    
+    # method to set time of ALMEMO 2490
+    @err_hdl.do_handle_exceptions()
+    def set_time(self):
+       
+        # request date from operating system
+        #convert requested time to strinf
+        # time format: hhmmss (h:hour, m: minute, s: second)
+       time = datetime.strftime(datetime.now(), "%H%M%S")
+       
+       # cmd string for setting time of device
+       cmd_str = 'U' + time
+       
+       # send command for setting date to device
+       self.query(cmd_str)
+       
+    
+    # method to get actual date from ALMEMO 2490 
+    @err_hdl.do_handle_exceptions(default_response = "01.01.70")
+    def get_date(self):
+        
+        # request date from ALMEMO 2490
+        read_buffer = self.query('P13')
+        
+        # search date string in reply to command P13 
+        # date alway has the following format: xx.xx.xx where x is within [0-9]
+        # get time out of read buffer and strip white space characters
+
+        date = re.search(r'([0-9]{2}\.[0-9]{2}\.[0-9]{2})', read_buffer).group(1)
+        
+        return date
+    
+    # method to get actual actual time from ALMEMO 2490
+    @err_hdl.do_handle_exceptions(default_response = "00:00:00")
+    def get_time(self):
+        
+        # request time from ALMEMO 2490
+        read_buffer = self.query('P10')
+        
+        # search time string in reply to command P10 
+        # time alway has the following format: xx:xx:xx where x cis within [0-9]
+        # get time out of read buffer and strip white space characters
+        time = re.search(r'([0-9]{2}:[0-9]{2}:[0-9]{2})', read_buffer).group(1)
+        
+        return time
+    
+    # method to set channel name of selected channel at ALMEMO 2490
+    # channel name can be at max 10 characters
+    # not allowed characters in name are white space charcters and semicolon
+    # numbmer format: 'xy'  where x has to be [0-9] and y has be [0-1]
+    @err_hdl.do_handle_exceptions()
+    def set_channel_name(self, channel, channel_name):
+        
+        # select channel for modification 
+        self.query('M' + channel)
+               
+        # send first part of command for changing channel name
+        self.query('f2')
+        
+        # send second part of command for changing channel name + channel name + CR
+        self.query('$' + channel_name + '\r')
+        
+        # call  method for requisting channel name from ALMEMO 2490
+        read_buffer = self.get_channel_name(channel)
+        
+        # compare requested channel name an channel name from parameter list
+        # if non equal print error message 
+        if read_buffer.strip() != channel_name:
+            print('setting of channel name has failed')
+        
+        
+    # method to get channel name from selected channel at ALMEMO 2490
+    # numbmer format: 'xy'  where x has to be [0-9] and y has be [0-1]
+    @err_hdl.do_handle_exceptions(default_response = "N/A")
+    def get_channel_name(self, channel):
+        
+        # select channel for request of channel name
+        self.query('M' + channel)
+        
+        # send command for requesting channel name to ALMEMO 2490
+        read_buffer = self.query('P00')
+        
+        
+        # search channel name in reply to cammand P00
+        # name follows after 2$\ in read_buffer
+        # get name out of read buffer 
+            
+        channel_name = re.search(r'([^ ]+ +\r)', read_buffer).group(1)
+ 
+        return channel_name
+    
+    
+    # method to request the list of all active measurement  channels of all sensors
+    # that are connected to ALMEMO 2490
+    # several parameters (channel number, meas_port, channel_unit, channel_name) are
+    # collected during request an stored in the meas buffer data frame of python class
+    @err_hdl.do_retry(tries=4, delay=3, backoff=3)
+    def request_sens_channel_list(self):
+        
+        self.set_meas_output_format('N0')
+        
+        # send command for requesting the list of all active meas channels of all sensors
+        # that are connected to ALMEMO 2490
+        read_buffer = self.query('P15')
+        
+        # find channel entries in read buffer and add them to a list
+        # a channel entry starts with M\ and ends with CR
+        channel_entries = re.findall(r'([0-9]{2}:DIGI.+\r)', read_buffer)
+        
+        # to get fixed parameter (channel number, measurment name, measurment unit),
+        # of a channel entry out of the read buffer search patterns are needed to identify them 
+    
+        search_patterns = {'sens_channel': r'([0-9]{2})', \
+                               'channel_unit': r'( [^ ]{2})', 'channel_name': r'([^ ]+ +\r)'}
+        
+        # the search pattern contains identification string for the search in front of the
+        # measurment parameter of interest and has to be removed -> strip pattern
+        strip_patterns = {'sens_channel': '', 'channel_unit': ' ', \
+                              'channel_name': ' \r'}            
+        
+        # dictonary where parameter of a channel are temporary store before they are added
+        # to the measurement buffer of the class
+        channel_params = {'sens_channel': '', 'channel_unit': '', 'channel_name': ' ', 'sens_port': ''}
+            
+        
+        for element in channel_entries:
+            
+            for key in search_patterns:
+                
+               
+                # for an channel entry in read buffer parameter is searched in the string,
+                # and the stip pattern is removed
+                # result of search and strip is stored in dictonary channel params 
+                channel_params.update({key: re.search(search_patterns[key] ,element).group(1)})
+                channel_params.update({key: channel_params[key].strip(strip_patterns[key])})
+                
+            
+            # with the information of sens_channel the sensor port is determined
+       
+            sens_port = channel_params['sens_channel'][1]
+            channel_params.update({'sens_port': 'M' + sens_port})
+            
+            # add a row in measurement buffer an fill colums in measurement buffer with
+            # collected parameter                        
+            self.meas_buffer = self.meas_buffer.append(channel_params, ignore_index= True)
+            
+            self.set_meas_output_format('N2')
+              
+        
+        
+    # method to request all measument values for all measurement channels of all sensors
+    # that are connected to ALMEMO 2490
+    # if request fails meas_buffer of class is not updated
+    @err_hdl.do_retry(tries=10, delay=3, backoff=3)
+    def request_meas_vals_all_channels(self, wait_time = 0.5):
+        
+        # request measured value from all measurment channels of all connected sensor
+        # to ALMEMO 2490
+        read_buffer = self.query('S1', wait_time)
+              
+        # search for line in measurment buffer that start with a 2 numbers 
+        meas_vals_string = re.search(r'([0-9]{2}[^\s]+)',read_buffer).group(1)
+
+
+        # change decimal designator to point if not default 
+        mod_meas_vals_string = meas_vals_string.replace(',','.')
+        
+        
+        # ALMEMO 2490 returns date, time and the measured values for all
+        # sensor channels of all connceted sensors.
+        # parameters are separated by semicolon, splitted and stored in list format
+        meas_param_list = re.findall(r'[^;"]+', mod_meas_vals_string)
+        
+        # extraction of date from meas_param_list
+        date = meas_param_list.pop(0)
+        date = date.strip('\"')
+        
+        # extraction of time from meas_param_list
+        time = meas_param_list.pop(0)
+        time = time.strip('\"')
+            
+        # check if extracted date and time have correct format
+        datetime.strptime(date, '%d.%m.%y')
+        datetime.strptime(time, '%H:%M:%S')
+        
+        # store date and time of measurement in meas_buffer of class
+        self.meas_buffer.loc[:,'meas_date'] = date
+        self.meas_buffer.loc[:,'meas_time'] = time
+    
+        
+        # after exctraction of date and time meas_vals_list does only contain
+        # the measured mavules for all sensor channels
+        # convert the measured values now to float
+        meas_vals_list = np.array(meas_param_list, dtype = 'float')
+        
+        # sore measured values for all sensor channels in measurment buffer of class
+        self.meas_buffer.loc[:, 'meas_val'] = meas_vals_list
+            
+        
+        
+    
+    # method to set the ouput format for the measured data
+    # default option and the only one at ALMEMO 2490 is
+    # output in table format separated by semicolon
+    # S1 for request replies the following format:
+    # date;time;meas value channel 1;meas vale channel 2; ...;meas value channel N 
+    @err_hdl.do_handle_exceptions() 
+    def set_meas_output_format(self, output_format = 'N2'):
+        
+        # send command to set the output format the measured data
+        self.query (output_format)
+        
+    # method to the the conversion rate (Measurements per Second) 
+    @err_hdl.do_handle_exceptions() 
+    def set_conversion_rate(self, rate = 10):
+        
+        # create as dictonary with the possible conversion rates and the 
+        # scan time in seconds for the conversion rate
+        scan_time_dict = dict({10: 0.9, 50: 0.18, 100: 0.09})
+        
+        # send command for setting conversion rate to ALMEMO 2490
+        self.query(int(rate))
+        
+        # fetch scan time for selected conversion rate out of dictonary 
+        scan_time = scan_time_dict.get(int(rate))
+        
+        return scan_time
+    
+    # method to fetch parameter for selected channel from measrement buffer of class
+    # selection possible by channel number of sensor channel or channel name
+    # if more than one row of channel buffer fits entry can be selected by optional parameter item
+    @err_hdl.do_handle_exceptions(default_response = (pd.Series(), 0))
+    def fetch_channel_param_from_meas_buffer(self, pattern = '00', index = 0):
+        
+        # filter rows of data frame that match to selection
+        # results will be a dataframe that contains all matches
+        sel_channel_data = self.meas_buffer[self.meas_buffer.isin([pattern]).any(axis=1)] 
+        
+        # if no entry for selection of sensor channel is found in meas buffer of class,
+        # sensor channel '0.0' will be selected as default parameter
+        if sel_channel_data.empty == True:
+            
+            sel_channel_data = self.meas_buffer[self.meas_buffer.isin(['00']).any(axis=1)]
+            
+            # info message is printed that no channel matches to selection
+            print('value is not in data_frame. Default value (channel 00) is returned')                                
+        
+        # determine number of matches
+        num_matches = sel_channel_data.shape[0]
+        
+        # check if selected entry of filtered measured buffer smaller than number of matches
+        # max index is always number of matches -1
+        if index < num_matches:
+            meas_channel_data = sel_channel_data.iloc[index,:]
+        
+        else:
+            
+            # determine the maximum value for index
+            # max index = number of matches -1
+            max_item_val = num_matches - 1
+            
+            # return default value which is the fisrt one in the filtered meas_buffer
+            # print error message that selected index is too high
+            meas_channel_data = sel_channel_data.iloc[0,:]
+            print('Value for index too large. Set Value to Default (index = 0)\n')
+            print('max index: %d' % max_item_val) 
+        
+        # data series with selected channel is returned containing all parameter of sensor channel
+        # if more entires match to selection data series that should be returned can be selected
+        # by optinal parameter item. By default item is set to 0
+        # second parameter that is returned by the method is the number of matches
+        # that fit to selection criteria
+        return meas_channel_data, num_matches 
+        
+    
+    # close telenet connection
+    def close(self):
+        if self.tn is not None:
+            self.tn.close()
+        
+    def __exit__(self):
+        self.close()
+    
+    # destructor of class
+    def __del__(self):
+        self.close()
+
+
+
+
+
+    
+    
diff --git a/Python_script/almemo710.py b/Python_script/almemo710.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd6a7cd6234c8cd46799f418bd5d2a1fd1e068b0
--- /dev/null
+++ b/Python_script/almemo710.py
@@ -0,0 +1,479 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Jan  2 11:08:12 2023
+
+@author: michael pawelzik
+Some parts have been copied from uros mavric and then they have been modified for compatibility. 
+
+#### SYNTAX FOR CALLING CLASS OBJECTS ########################################
+
+Look into module play_with_almemo710.py
+
+##############################################################################
+"""
+
+
+# import pythod modules
+import time
+from telnetlib import Telnet
+import re
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import traceback
+from functools import wraps
+
+
+# python class for error handling of almemo 710
+class err_hdl(Exception):
+    
+    # function for retry decorate object in case of exception
+    def do_retry(ExceptionToCheck = Exception, tries=4, delay=3, backoff=2, \
+                 default_response = None):
+        """Retry calling the decorated function using an backoff and defaults for return
+    
+        original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
+        several lines of code have been modified for functionality
+        
+        :param ExceptionToCheck: the exception to check. may be a tuple of
+            exceptions to check
+        :type ExceptionToCheck: Exception or tuple
+        :param tries: number of times to try (not retry) before giving up
+        :type tries: int
+        :param delay: initial delay between retries in seconds
+        :type delay: int
+        :param backoff: backoff multiplier e.g. value of 2 will double the delay
+            each retry
+        :param default response: default values to return if all tries fail
+        : type default response: Value or tuple
+        
+        """
+        def decorator_retry(function):
+    
+            @wraps(function)
+            def function_retry(*args, **kwargs):
+                
+                mtries, mdelay = tries, delay
+                while mtries > 0:
+                    try:
+                        return function(*args, **kwargs)
+                    except ExceptionToCheck:
+                        print("ALMEO710: Error occured in %s,'Retrying in %d seconds" %(function.__name__, mdelay))
+                        time.sleep(mdelay)
+                        mtries -= 1
+                        mdelay += backoff
+                raise Exception('maximum number of retries [%d] reached\nfunction: %s\nmodule: %s' \
+                      % (tries, function.__name__,function.__module__)) 
+                
+            return function_retry # true decorator
+            
+        return decorator_retry
+    
+    # function for handle exception
+    def do_handle_exceptions(ExceptionToCheck = Exception, default_response = None):
+        
+        """ error calling the decorated function using defaults for return
+        original from: 
+        https://www.3resource.com/python-excercises/decorator/python-decorator-exercise-9.php
+        
+        several lines of code have been modified for functionality
+        :param ExceptionToCheck: the exception to check. may be a tuple of
+            exceptions to check
+        :type ExceptionToCheck: Exception or tuple    
+        :param default response: default values to return if all tries fail
+        : type default response: Value or tuple
+            
+        """
+        def exception_decorator(function):
+            
+            def function_handle_exception(*args, **kwargs):
+                
+                try:
+                    return function(*args, **kwargs)
+                
+                except ExceptionToCheck as e:
+                    
+                    print('ALMEO710: ' + str(type(e).__name__) + " occured in:\nfunction: %s\nmodule: %s" \
+                          % (function.__name__,function.__module__))
+                    print(traceback.print_tb(e.__traceback__))
+    
+                    return default_response
+    
+            return function_handle_exception
+        
+        return exception_decorator
+
+
+
+class almemo710:
+   
+    
+    # constructor of class
+    def __init__(self, ip = '192.168.115.94', timeout = 10):
+        self.tn = None
+        
+        # set class variabel with ip adress given in parameter list of constructor
+        self.ip_adress = ip 
+        
+        # set class variable with timeout value given in parameter list of constructor
+        self.timeout = timeout
+        
+        # open telenet connection to ALMEMO 710
+        self.tn = self.open_connection()
+
+        # create data frame for measurment buffer to store parameter and measured
+        # properties of connected sensors to ALMEMO 710
+        self.meas_buffer = pd.DataFrame(columns = ['meas_date','meas_time', 'sens_port', \
+                                                   'sens_channel', 'meas_val', 'channel_unit', \
+                                                   'channel_name'])
+   
+   
+
+    """
+    ##### commands are echoed. Will cause conflicts in communication and there it is obmitted #### 
+    # write function to send commands to ALMEMO 710 that do not send a
+    # response
+    def write(self, cmd_str, wait_time = 0.5):
+     
+        # send command to ALMEMO 710
+        self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r')
+     
+        # wait
+        time.sleep(wait_time)
+    """ 
+    
+    @err_hdl.do_retry(tries=5, delay=1, backoff=3)
+    def open_connection(self):
+        return Telnet(self.ip_adress, 10001, self.timeout)
+    
+    # query function to send a command to almemo 710 and
+    # return the response to the command
+    
+    def query(self, cmd_str, wait_time = 1):
+        
+        # send request to ALMEMO 710 encode UTF8 with CR
+        self.tn.write(bytes(cmd_str, encoding = 'utf8') + b'\r')
+        
+        # wait
+        time.sleep(wait_time)
+        
+        # read_buffer = self.tn.read_very_eager()
+        # read buffer until END OF TEXT character has been reach or timeout has occured
+        read_buffer = self.tn.read_until(b'\x03',self.timeout)
+        
+        # decode receivved data with CP437 format
+        decoded_read_buffer = read_buffer.decode('cp437')
+        
+        # return received data
+        return decoded_read_buffer
+   
+
+   # method to set date of ALMEMO 710
+    @err_hdl.do_handle_exceptions() 
+    def set_date(self):
+        
+       # request date from operating system
+       # convert requested date to string
+       # date format: ddmmyy (d: day, m: month, y: year, last 2 digits )
+       date = datetime.strftime(datetime.now(),"%d%m%y")
+        
+       # cmd string for setting date of device
+       cmd_str = 'd' + date
+       
+       # send command for setting date to device 
+       self.query(cmd_str)
+       
+   
+    
+    # method to set time of ALMEMO 710
+    @err_hdl.do_handle_exceptions()
+    def set_time(self):
+       
+        # request date from operating system
+        #convert requested time to strinf
+        # time format: hhmmss (h:hour, m: minute, s: second)
+       time = datetime.strftime(datetime.now(), "%H%M%S")
+       
+       # cmd string for setting time of device
+       cmd_str = 'U' + time
+       
+       # send command for setting date to device
+       self.query(cmd_str)
+       
+    
+    # method to get actual date from ALMEMO 710
+    @err_hdl.do_handle_exceptions(default_response = "01.01.1970")
+    def get_date(self):
+        
+        # request date from ALMEMO 710
+        read_buffer = self.query('P13')
+        
+        # search date string in reply to command P13 
+        # date alway has the following format: xx.xx.xx where x is within [0-9]
+        # get time out of read buffer and strip white space characters
+        
+        date = re.search(r'([0-9]{2}\.[0-9]{2}\.[0-9]{2})', read_buffer).group()
+        
+        return date
+
+    # method to get actual actual time from ALMEMO 710
+    @err_hdl.do_handle_exceptions(default_response = "00:00:00")
+    def get_time(self):
+        
+        # request time from ALMEMO 710
+        read_buffer = self.query('P10')
+        
+        # search time string in reply to command P10 
+        # time alway has the following format: xx:xx:xx where x cis within [0-9]
+        # get time out of read buffer and strip white space characters
+        
+        time = re.search(r'([0-9]{2}:[0-9]{2}:[0-9]{2})', read_buffer).group()
+        
+        return time
+    
+    # method to set channel name of selected channel at ALMEMO 710
+    # channel name can be at max 10 characters
+    # not allowed characters in name are white space charcters and semicolon
+    # channel numbers: string, format 'x.x' where x has to be within [0-9]
+    @err_hdl.do_handle_exceptions()
+    def set_channel_name(self, channel, channel_name):
+        
+        # select channel for modification 
+        self.query('M' + channel)
+               
+        # send first part of command for changing channel name
+        self.query('f2')
+        
+        # send second part of command for changing channel name + channel name + CR
+        self.query('$' + channel_name + '\r')
+        
+        # call  method for requisting channel name from ALMEMO 710
+        read_buffer = self.get_channel_name(channel)
+        
+        # compare requested channel name an channel name from parameter list
+        # if non equal print error message 
+        if read_buffer.strip() != channel_name:
+            print('setting of channel name has failed')
+        
+        
+    # method to get channel name from selected channel at ALMEMO 710
+    # channel numbers: string, format 'x.x' where x has to be within [0-9]
+    @err_hdl.do_handle_exceptions(default_response = "N/A")
+    def get_channel_name(self, channel):
+        
+        # select channel for request of channel name
+        self.query('M' + channel)
+        
+        # send command for requesting channel name to ALMEMO 710
+        read_buffer = self.query('P00')
+        
+        # search channel name in reply to cammand P00
+        # name follows after 2$\ in read_buffer
+        # get name out of read buffer 
+        channel_name = re.search(r'(2\$\\[^;]+)', read_buffer).group()
+        
+        # strip white space characters
+        channel_name = channel_name.strip(r'(2\$\\)')
+        
+        return channel_name
+    
+    
+    # method to request the list of all active measurement  channels of all sensors
+    # that are connected to ALMEMO 710
+    # several parameters (channel number, meas_port, channel_unit, channel_name) are
+    # collected during request an stored in the meas buffer data frame of python class
+    @err_hdl.do_retry(tries=4, delay=3, backoff=3)
+    def request_sens_channel_list(self):
+        
+        # send command for requesting the list of all active meas channels of all sensors
+        # that are connected to ALMEMO 710
+        read_buffer = self.query('P15')
+        
+        # find channel entries in read buffer and add them to a list
+        # a channel entry starts with M\ and ends with CR
+        channel_entries = re.findall(r'(M\\.+\r)', read_buffer)
+        
+        # to get fixed parameter (channel number, measurment name, measurment unit),
+        # of a channel entry out of the read buffer search patterns are needed to identify them 
+        # pattern for sens_channel: M\x.x where x within [0-9]
+        # pattern for channel_unit: 1$\xx where x can be every character
+        # pattern for channel_name: 2$\xx where x can be every character without semicolon
+        search_patterns = {'sens_channel': r'(M\\[0-9]+\.{1}[0-9]+)', \
+                               'channel_unit': r'(1\$\\.{2})', 'channel_name': r'(2\$\\[^;]*)'}
+        
+        # the search pattern contains identification string for the search in front of the
+        # measurment parameter of interest and has to be removed -> strip pattern
+        strip_patterns = {'sens_channel': r'(M\\)', 'channel_unit': r'(1\$\\)', \
+                              'channel_name': r'(2\$\\)'}            
+        
+        # dictonary where parameter of a channel are temporary store before they are added
+        # to the measurement buffer of the class
+        channel_params = {'sens_channel': '', 'channel_unit': '', 'channel_name': '', 'sens_port': ''}
+            
+        
+        for element in channel_entries:
+            
+            for key in search_patterns:
+                
+                # for an channel entry in read buffer parameter is searched in the string,
+                # and the stip pattern is removed
+                # result of search and strip is stored in dictonary channel params 
+                channel_params.update({key: re.search(search_patterns[key] ,element).group(1)})
+                channel_params.update({key: channel_params[key].strip(strip_patterns[key])})
+                
+            
+            # with the information of sens_channel the sensor port is determined
+            # sens_port = 'M' + numbers in front of the point of the measurment channel
+            # example: sens_channel = 0.1 -> sens_port = M0 
+            sens_port, _ = channel_params['sens_channel'].split('.')
+            channel_params.update({'sens_port': 'M' + sens_port})
+            
+            # add a row in measurement buffer an fill colums in measurement buffer with
+            # collected parameter                        
+            self.meas_buffer = self.meas_buffer.append(channel_params, ignore_index= True)
+                  
+        
+        
+    # method to request all measument values for all measurement channels of all sensors
+    # that are connected to ALMEMO 710
+    # if request fails meas_buffer of class is not updated
+    
+    @err_hdl.do_retry(tries=10, delay=3, backoff=3)
+    def request_meas_vals_all_channels(self, wait_time = 0.5):
+        
+        # request measured value from all measurment channels of all connected sensor
+        # to ALMEMO 710
+        read_buffer = self.query('S1', wait_time)
+        
+            
+        # search for line in measurment buffer that starts with digit and ends with digit  
+        meas_vals_string = re.search(r'([0-9].+[0-9])',read_buffer).group()
+        
+        # change decimal designator to point if not default 
+        mod_meas_vals_string = meas_vals_string.replace(',','.')
+        
+        # ALMEMO 710 returns date, time and the measured values for all
+        # sensor channels of all connceted sensors.
+        # parameters are separated by semicolon, splitted and stored in list format
+        meas_param_list = mod_meas_vals_string.split(';')
+        
+        # extraction of date from meas_param_list
+        date = meas_param_list.pop(0)
+        
+        # extraction of time from meas_param_list
+        time = meas_param_list.pop(0)
+        
+            
+        # check if extracted date and time have correct format
+        datetime.strptime(date, '%d.%m.%y')
+        datetime.strptime(time, '%H:%M:%S')
+        
+        # store date and time of measurement in meas_buffer of class
+        self.meas_buffer.loc[:,'meas_date'] = date
+        self.meas_buffer.loc[:,'meas_time'] = time
+        
+        
+        # after exctraction of date and time meas_vals_list does only contain
+        # the measured mavules for all sensor channels
+        # convert the measured values now to float
+        meas_vals_list = np.array(meas_param_list, dtype = 'float')
+        
+        # sore measured values for all sensor channels in measurment buffer of class
+        self.meas_buffer.loc[:, 'meas_val'] = meas_vals_list
+        
+        
+    
+    # method to set the ouput format for the measured data
+    # default option and the only one at ALMEMO 710 is
+    # output in table format separated by semicolon
+    # S1 for request replies the following format:
+    # date;time;meas value channel 1;meas vale channel 2; ...;meas value channel N 
+    @err_hdl.do_handle_exceptions() 
+    def set_meas_output_format(self, output_format = 'N2'):
+        
+        # send command to set the output format the measured data
+        self.query (output_format)
+        
+    # method to the the conversion rate (Measurements per Second) 
+    @err_hdl.do_handle_exceptions() 
+    def set_conversion_rate(self, rate = 10):
+        
+        # create as dictonary with the possible conversion rates and the 
+        # scan time in seconds for the conversion rate
+        scan_time_dict = dict({10: 0.9, 50: 0.18, 100: 0.09})
+        
+        # send command for setting conversion rate to ALMEMO 710
+        self.query(int(rate))
+        
+        # fetch scan time for selected conversion rate out of dictonary 
+        scan_time = scan_time_dict.get(int(rate))
+        
+        return scan_time
+    
+    # method to fetch parameter for selected channel from measrement buffer of class
+    # selection possible by channel number of sensor channel or channel name
+    # if more than one row of channel buffer fits entry can be selected by optional parameter item
+    @err_hdl.do_handle_exceptions(default_response = (pd.Series(), 0))
+    def fetch_channel_param_from_meas_buffer(self, pattern = '0.0', index = 0):
+        
+        # filter rows of data frame that match to selection
+        # results will be a dataframe that contains all matches
+        sel_channel_data = self.meas_buffer[self.meas_buffer.isin([pattern]).any(axis=1)] 
+        
+        # if no entry for selection of sensor channel is found in meas buffer of class,
+        # sensor channel '0.0' will be selected as default parameter
+        if sel_channel_data.empty == True:
+            
+            sel_channel_data = self.meas_buffer[self.meas_buffer.isin(['0.0']).any(axis=1)]
+            
+            # info message is printed that no channel matches to selection
+            print('value is not in data_frame. Default value (channel 0.0) is returned')                                
+        
+        # determine number of matches
+        num_matches = sel_channel_data.shape[0]
+        
+        # check if selected entry of filtered measured buffer smaller than number of matches
+        # max index is always number of matches -1
+        if index < num_matches:
+            meas_channel_data = sel_channel_data.iloc[index,:]
+        
+        else:
+            
+            # determine the maximum value for index
+            # max index = number of matches -1
+            max_item_val = num_matches - 1
+            
+            # return default value which is the fisrt one in the filtered meas_buffer
+            # print error message that selected index is too high
+            meas_channel_data = sel_channel_data.iloc[0,:]
+            print('Value for index too large. Set Value to Default (index = 0)\n')
+            print('max index: %d' % max_item_val) 
+        
+        # data series with selected channel is returned containing all parameter of sensor channel
+        # if more entires match to selection data series that should be returned can be selected
+        # by optinal parameter item. By default item is set to 0
+        # second parameter that is returned by the method is the number of matches
+        # that fit to selection criteria
+        return meas_channel_data, num_matches 
+    
+        
+    # close telenet connection
+    def close(self):
+        if self.tn is not None:
+            self.tn.close()
+    
+    def __exit__(self):
+        self.close()
+        
+    # destructor of class
+    def __del__(self):
+        self.close()
+
+
+
+
+
+
+
+    
+    
diff --git a/Python_script/analysis.py b/Python_script/analysis.py
index 5ec2edecbfb68a5b734ba66c239d66ab4ee89d2c..0d20ffeed20010795d88c6f5e9f19e96f5f2fbdf 100644
--- a/Python_script/analysis.py
+++ b/Python_script/analysis.py
@@ -1,88 +1,205 @@
 import pandas as pd
 import matplotlib.pyplot as plt
-import math
+import numpy as np
+from matplotlib import gridspec
+from SoftwareVersion import software_version
 
-def extract_stable_data(datafile):
+def extract_stable_data(datafile, measurement_set, reference_signal_names, extra_signal_names):
     datapoint = {}
-    #df is a pandas data frame
+    # df is a pandas data frame
     df = pd.read_csv(datafile)
 
-    #extract phase mean and variance for stable measurements (don't ask what loc means) 
-    phases = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'S21_PHASE']
-    if phases.size == 0:
+    # extract phase mean and variance for stable measurements (don't ask what loc means)
+    signal0 = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set),
+                     reference_signal_names[0]]
+    if signal0.size == 0:
         return None
-    datapoint['phase_mean']=phases.mean()
-    datapoint['phase_var']=phases.var()
+    datapoint['signal0_mean']=signal0.mean()
+    datapoint['signal0_var']=signal0.var()
 
-    magnitudes = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'S21_MAGNITUDE']
-    datapoint['magnitude_mean'] = magnitudes.mean()
-    datapoint['magnitude_var'] = magnitudes.var()
+    signal1 = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set),
+                     reference_signal_names[1]]
+    datapoint['signal1_mean'] = signal1.mean()
+    datapoint['signal1_var'] = signal1.var()
 
-    temperatures = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'READBACK_TEMPERATURE']
+    temperatures = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'TEMP_DUT']
     datapoint['temperature_mean'] = temperatures.mean()
     datapoint['temperature_var'] = temperatures.var()
 
-    humidities = df.loc[df['EQUILIBRIUM_INDICATOR'] == 31, 'READBACK_HUMIDITY']
+    humidities = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), 'HUM_DUT']
     datapoint['humidity_mean'] = humidities.mean()
     datapoint['humidity_var'] = humidities.var()
 
+    for extra_signal_name in extra_signal_names:
+        extra_signal_values = df.loc[(df['EQUILIBRIUM_INDICATOR'] == 31) & (df['SET_NAME'] == measurement_set), extra_signal_name]
+        datapoint[extra_signal_name] = extra_signal_values.mean()
+
     return datapoint
 
 
 # sweep_type is either 'temperature' or 'humidity'
-def plot_sweep(temperatures, humidities, basename, sweep_type):
-    x_data = []
-    phases = []
-    phase_vars = []
-    magnitudes = []
-    magnitude_vars = []
+def plot_sweep(temperatures, humidities, basename, sweep_type, measurement_sets, reference_signal_names,
+               analysis_config):
+    set_data = {}
+    derivatives = {}
+    for measurement_set in measurement_sets:
+        set_data[measurement_set] = {'signal0_means': [], 'signal0_vars': [], 'signal1_means': [], 'signal1_vars': [],
+                                     'x_data': []}
+        for external_signal_name in analysis_config['extra_signal_names']:
+            set_data[measurement_set][external_signal_name] = []
+        derivatives[measurement_set] = {'signal0_deltas': [], 'signal1_deltas': [], 'x_deltas': []}
+        
     for temp, hum in zip(temperatures, humidities):
         datafile = basename+'_'+str(temp)+'deg_'+str(hum)+'rh.csv'
         print(datafile)
-        datapoint = extract_stable_data(datafile)
-        if datapoint is None:
-            continue
-
-        if sweep_type == 'temperature':
-            x_data.append(datapoint['temperature_mean'])
-        elif sweep_type == 'humidity':
-            x_data.append(datapoint['humidity_mean'])
-        else:
-            raise Exception('Unknown sweep_type:'+str(sweep_type))
-
-        phases.append(datapoint['phase_mean'])
-        phase_vars.append(datapoint['phase_var'])
-        magnitudes.append(datapoint['magnitude_mean'])
-        magnitude_vars.append(datapoint['magnitude_var'])
-
-    magnitudes_db = [20 * math.log10(x) for x in magnitudes ]
-    # approximate formula for small errors (linear approximation)
-    magnitude_vars_db = [20 / math.log(10) * dx/x for x, dx in zip(magnitudes,  magnitude_vars)]
+        for measurement_set in measurement_sets:
+            datapoint = extract_stable_data(datafile, measurement_set, reference_signal_names,
+                                            analysis_config['extra_signal_names'])
+            if datapoint is None:
+                continue
+
+            data = set_data[measurement_set]
+            if sweep_type == 'temperature':
+                data['x_data'].append(datapoint['temperature_mean'])
+            elif sweep_type == 'humidity':
+                data['x_data'].append(datapoint['humidity_mean'])
+            else:
+                raise Exception('Unknown sweep_type:'+str(sweep_type))
+
+            data['signal0_means'].append(datapoint['signal0_mean'])
+            data['signal0_vars'].append(datapoint['signal0_var'])
+            data['signal1_means'].append(datapoint['signal1_mean'])
+            data['signal1_vars'].append(datapoint['signal1_var'])
+
+            for external_signal_name in analysis_config['extra_signal_names']:
+                data[external_signal_name].append(datapoint[external_signal_name])
+
+    for measurement_set in measurement_sets:
+        data = set_data[measurement_set]
+        if analysis_config['normalise'][0]:
+            data['signal0_means'] -= data['signal0_means'][0]
+        if analysis_config['normalise'][1]:
+            data['signal1_means'] -= data['signal1_means'][0]
+
+    fig = plt.figure(figsize=(10, 15))
+    gs = gridspec.GridSpec(nrows=6, ncols=1, hspace=0)#, width_ratios=[3, 1], height_ratios=[3, 1])
+    
+    upper_text_block = '\n'.join(['DUT name: '+analysis_config['dut_name'],
+                                  'SW version: '+software_version(), 'Date: '+analysis_config['time_string']])
 
-    fig, ax1 = plt.subplots()
+    
+    fig.text(0.1, 0.88, upper_text_block)
+    
+    ax1 = fig.add_subplot(gs[1, 0])
+    ax2 = fig.add_subplot(gs[2, 0], sharex=ax1)
+    ax4 = fig.add_subplot(gs[4, 0])
+    ax5 = fig.add_subplot(gs[5, 0], sharex=ax4)
+    
+    ax1.tick_params(bottom=True, top=True, direction='in')
+    ax2.tick_params(bottom=True, top=True, direction='inout')
+    ax4.tick_params(bottom=True, top=True, direction='in')
+    ax5.tick_params(bottom=True, top=True, direction='inout')
 
     if sweep_type == 'temperature':
-        plt.title(basename + ': Temperature sweep at ' + str(humidities[0]) + ' % r.h.')
-        ax1.set_xlabel('temperature [deg C]')
+        fig.suptitle(basename + ': Temperature sweep at ' + str(humidities[0]) + ' % r.h.')
+        ax2.set_xlabel('temperature [deg C]')
+        ax5.set_xlabel('temperature [deg C]')
+        denominator_name = '/ deg C'
     elif sweep_type == 'humidity':
-        plt.title(basename + ': Humidity sweep at ' + str(temperatures[0]) + ' deg C')
-        ax1.set_xlabel('relative humidity [%]')
+        fig.suptitle(basename + ': Humidity sweep at ' + str(temperatures[0]) + ' deg C')
+        ax2.set_xlabel('relative humidity [%]')
+        ax5.set_xlabel('relative humidity [%]')
+        denominator_name = '/ % r.h.'
     else:
         raise Exception('Unknown sweep_type:'+str(sweep_type))
 
-    ax1.errorbar(x_data, phases, phase_vars, marker='+', linewidth=0)
-    ax1.set_ylabel('S21 phase [deg]', color='blue')
-
-    ax2 = ax1.twinx()
-    ax2.errorbar(x_data, magnitudes_db, magnitude_vars_db, marker='x', color='red', linewidth=0)
-    ax2.set_ylabel('S21 magnitude [dB]', color='red')
+    for measurement_set in measurement_sets:
+        ax1.errorbar(set_data[measurement_set]['x_data'],
+                     set_data[measurement_set]['signal0_means'], set_data[measurement_set]['signal0_vars'],
+                     label=measurement_set, marker='+', linewidth=0)
+    ax1.set_ylabel(reference_signal_names[0])
+
+    for measurement_set in measurement_sets:
+        ax2.errorbar(set_data[measurement_set]['x_data'],
+                     set_data[measurement_set]['signal1_means'], set_data[measurement_set]['signal1_vars'],
+                     label=measurement_set, marker='+', linewidth=0)
+    ax2.set_ylabel(reference_signal_names[1])
+
+    # Remove the highest tick label from the lower subplot. It probably overlaps with the lowest one of the upper plot.
+    yticks = ax2.yaxis.get_major_ticks()
+    yticks[-1].set_visible(False)
+    yticks[-2].label1.set_visible(False)  # don't ask me why we have to set the last two tick marks to invisible
+    ax2.legend(loc='center right', bbox_to_anchor=(1.5, 1.0))
+
+    #######################################################################################################################
+    # Derrivatives
+    #######################################################################################################################
+    for measurement_set in measurement_sets:
+        data = set_data[measurement_set]
+        deriv_data = derivatives[measurement_set]
+        for i in range(len(data['signal0_means'])-1):
+            ylabel = '$\\Delta$ '+reference_signal_names[0]+' '+denominator_name
+            deriv_data['signal0_deltas'].append((data['signal0_means'][i+1]-data['signal0_means'][i])/
+                                                (data['x_data'][i+1]-data['x_data'][i]))
+            deriv_data['signal1_deltas'].append((data['signal1_means'][i+1]-data['signal1_means'][i])/
+                                                (data['x_data'][i+1]+data['x_data'][i]))
+            deriv_data['x_deltas'].append((data['x_data'][i+1]+data['x_data'][i])/2)
+
+    ax4.set_ylabel('$\\Delta$ '+reference_signal_names[0]+' '+denominator_name)
+    ax5.set_ylabel('$\\Delta$ '+reference_signal_names[1]+' '+denominator_name)
+
+    #######################################################################################################################
+    # RF cable post analysis: Normalise to cable length and convert phase to time
+    #######################################################################################################################
     
-    fig.tight_layout()  # otherwise the right y-label is slightly clipped
-
-    fig.savefig(basename+'_analysis.pdf')
+    if analysis_config['type'] == 'rf_cable':
+        for measurement_set in measurement_sets:
+            data = set_data[measurement_set]
+            deriv_data = derivatives[measurement_set]
+            phase_to_time = 1e15 / (analysis_config['cable_length'] * data['RF_FREQUENCY'][0] * 360.)
+            deriv_data['signal0_deltas'][:] = [x * phase_to_time for x in deriv_data['signal0_deltas']]
+            deriv_data['signal1_deltas'][:] = [x / analysis_config['cable_length'] for x in deriv_data['signal1_deltas']]
+
+        ax4.set_ylabel('$\\Delta$t [fs/m/K]')
+        ax5.set_ylabel('$\\Delta$A [dB/m/K]')
+
+
+    #######################################################################################################################
+    # Closeout
+    #######################################################################################################################
+    #manual scaling if y axis. Auto is broken with scatter.
+    ax4_mins = []
+    ax4_maxs = []
+    ax5_mins = []
+    ax5_maxs = []
+
+    for measurement_set in measurement_sets:
+        ax4.scatter(derivatives[measurement_set]['x_deltas'], derivatives[measurement_set]['signal0_deltas'], marker='+')
+        ax5.scatter(derivatives[measurement_set]['x_deltas'], derivatives[measurement_set]['signal1_deltas'], marker='+')
+        if not len(derivatives[measurement_set]['signal0_deltas']) == 0:
+            ax4_mins.append(min(derivatives[measurement_set]['signal0_deltas']))
+            ax4_maxs.append(max(derivatives[measurement_set]['signal0_deltas']))
+            ax5_mins.append(min(derivatives[measurement_set]['signal1_deltas']))
+            ax5_maxs.append(max(derivatives[measurement_set]['signal1_deltas']))
+
+    # set auto y range which does not work properly for small values in scatter
+    if not len(ax4_maxs) == 0:
+        dy4 = (max(ax4_maxs) - min(ax4_mins))*0.1
+        ax4.set_ylim(min(ax4_mins)-dy4, max(ax4_maxs)+dy4)
+        dy5 = (max(ax5_maxs) - min(ax5_mins))*0.1
+        ax5.set_ylim(min(ax5_mins)-dy5, max(ax5_maxs)+dy5)
+
+    fig.tight_layout()  # otherwise the legend is clipped
+
+    fig.savefig(basename+measurement_set+'_analysis.pdf')
     
     plt.show()
 
 
 if __name__ == '__main__':
-   plot_sweep(range(20, 30+1), [40]*11, 'tempsweep1', 'temperature')
+    print('run \'./prototype.py -t first_tempsweep.txt -p -o tempsweep1\' to get the data needed for this plot.' )
+    plot_sweep(np.arange(20., 31.+1.), [35.]*12, 'tempsweep1', 'temperature',
+               ['1.3GHz', '1.0GHz', '3.0GHz', '6.0GHz', '10.0GHz'], ['S21_PHASE', 'S21_MAGNITUDE'],
+               analysis_config={'type': 'rf_cable', 'normalise': [True, False], 'cable_length': 10,
+                                'extra_signal_names': ['RF_FREQUENCY'], 'dut_name': 'simulated cable',
+                                'time_string': '0:0:0'})
diff --git a/Python_script/climate-lab-gui.py b/Python_script/climate-lab-gui.py
index 6a9e19205209824f544c181039bb346b028dd525..2d63f4ff2a03b7824814ec99f6bac448caeca90a 100755
--- a/Python_script/climate-lab-gui.py
+++ b/Python_script/climate-lab-gui.py
@@ -54,6 +54,22 @@ class TestStandMainWindow(QMainWindow):
         print('changed dir')
         return True
 
+    def get_analysis_config(self, time_string):
+        analysis_config = {}
+        analysis_config['dut_name'] = self.dutName.text()
+        analysis_config['time_string'] = time_string
+        if self.analysisTypeCableButton.isChecked():
+            analysis_config['type'] = 'rf_cable'
+            analysis_config['extra_signal_names'] = ['RF_FREQUENCY']
+            analysis_config['normalise'] = [True, False]
+            analysis_config['cable_length'] = self.cableLengthSpinBox.value()
+        else:
+            analysis_config['type'] = 'default'
+            analysis_config['extra_signal_names'] = []
+            analysis_config['normalise'] = [self.removeOffsetsSignal0.isChecked(), self.removeOffsetsSignal1.isChecked()]
+
+        return analysis_config
+
     def do_measurement(self):
         self.setEnabled(False)
         self.qt_app.processEvents();
@@ -61,9 +77,13 @@ class TestStandMainWindow(QMainWindow):
         os.chdir(self.start_dir)
         with open('test_stand_parameter.json', 'r') as f:
             config_data = json.load(f)
+       
+        with open('ext_sensor_channels.json', 'r') as f2:
+            ext_sensor_channels = json.load(f2)
 
+        time_string = time.strftime("%Y_%m_%d-%H_%M_%S")
         if self.autoNameCheckbox.isChecked():
-            output_basename = time.strftime("%Y_%m_%d-%H_%M_%S") + "_results"
+            output_basename = time_string + "_results"
         else:
             output_basename = self.baseName.text()
 
@@ -71,9 +91,9 @@ class TestStandMainWindow(QMainWindow):
             self.setEnabled(True)
             return
 
-        meas = prototype.Measurements(config_data['chamber_ip'], config_data['vna_ip'], output_basename,
-                                      False, config_data)
+        meas = None
         try:
+            meas = prototype.Measurements(config_data, output_basename,False, ext_sensor_channels)
             if self.tempSweepButton.isChecked():
                 temperatures = meas.perform_sweep(self.startParameter.value(), self.stopParameter.value(),
                                                   self.stepParameter.value(), self.fixedParameter.value(),
@@ -83,9 +103,12 @@ class TestStandMainWindow(QMainWindow):
                 for t in temperatures:
                     temp_extensions.append(str(t) + 'deg_' + str(self.fixedParameter.value()) + 'rh')
                 analysis.plot_sweep(temperatures, [self.fixedParameter.value()] * len(temperatures), output_basename,
-                                    'temperature')
-                prototype.plot_output(output_basename, temp_extensions, True, output_basename +
-                                      ': Temperature sweep ' + str(temperatures[0]) + '--' +
+                                    'temperature', meas.dut.get_measurement_set_names(),
+                                    meas.dut.get_dut_reference_signal_names(), self.get_analysis_config(time_string))
+                
+                prototype.plot_output(output_basename, temp_extensions, True, config_data, ext_sensor_channels,
+                                      meas.dut.get_measurement_set_names(), meas.dut.get_dut_reference_signal_names(),
+                                      output_basename + ': Temperature sweep ' + str(temperatures[0]) + '--' +
                                       str(temperatures[-1]) + ' degC @ ' + str(self.fixedParameter.value()) + ' % r.h.')
 
             elif self.humSweepButton.isChecked():
@@ -97,22 +120,34 @@ class TestStandMainWindow(QMainWindow):
                 for h in humidities:
                     hum_extensions.append(str(self.fixedParameter.value()) + 'deg_' + str(h) + 'rh')
                 analysis.plot_sweep([self.fixedParameter.value()] * len(humidities), humidities, output_basename,
-                                    'humidity')
-                prototype.plot_output(output_basename, hum_extensions, True, output_basename +
-                                      ': Humidity sweep ' + str(humidities[0]) + '--' +
+                                    'humidity', meas.dut.get_measurement_set_names(),
+                                    meas.dut.get_dut_reference_signal_names(), self.get_analysis_config(time_string))
+                                
+                prototype.plot_output(output_basename, hum_extensions, True, config_data, ext_sensor_channels,
+                                      meas.dut.get_measurement_set_names(), meas.dut.get_dut_reference_signal_names(),
+                                      output_basename + ': Humidity sweep ' + str(humidities[0]) + '--' +
                                       str(humidities[-1]) + ' % r.h. @ ' + str(self.fixedParameter.value()) + ' degC')
 
             elif self.measurementFileButton.isChecked():
                 try:
                     n_measurements = meas.perform_measurements(os.path.join(self.start_dir,
                                                                             self.measurementFile.text()))
-                    prototype.plot_output(output_basename, range(n_measurements), True, output_basename)
+                    prototype.plot_output(output_basename, range(n_measurements), True, config_data, ext_sensor_channels,
+                                          meas.dut.get_measurement_set_names(),
+                                          meas.dut.get_dut_reference_signal_names(),
+                                          output_basename)
                 except FileNotFoundError as e:
                     QtWidgets.QMessageBox.warning(self, 'Warning', str(e))
 
+        except Exception as e:
+            print('ERROR: Exception during measurement: '+str(e))
+            QtWidgets.QMessageBox.critical(self, 'Error', str(e))
 
         finally:
-            meas.chamber.close()
+            if meas is not None:
+                meas.chamber.close()
+                meas.ext_sensors.close()
+            
             self.setEnabled(True)
 
 
diff --git a/Python_script/climate-lab-main.ui b/Python_script/climate-lab-main.ui
index 098b07eba123626d1333616a987a7d10eedf0522..9f90c667e844b1814fe3f085f162332c05347749 100644
--- a/Python_script/climate-lab-main.ui
+++ b/Python_script/climate-lab-main.ui
@@ -6,16 +6,16 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>618</width>
-    <height>603</height>
+    <width>536</width>
+    <height>786</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>MainWindow</string>
   </property>
   <widget class="QWidget" name="centralwidget">
-   <layout class="QGridLayout" name="gridLayout_4">
-    <item row="0" column="0">
+   <layout class="QVBoxLayout" name="verticalLayout_8">
+    <item>
      <widget class="QFrame" name="measurementTypeFrame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
@@ -74,7 +74,7 @@
       </layout>
      </widget>
     </item>
-    <item row="1" column="0">
+    <item>
      <spacer name="verticalSpacer_2">
       <property name="orientation">
        <enum>Qt::Vertical</enum>
@@ -87,7 +87,7 @@
       </property>
      </spacer>
     </item>
-    <item row="2" column="0">
+    <item>
      <widget class="QGroupBox" name="parametersGroupBox">
       <property name="title">
        <string>Sweep parameters</string>
@@ -191,7 +191,7 @@
              <number>1</number>
             </property>
             <property name="value">
-             <double>45.000000000000000</double>
+             <double>55.000000000000000</double>
             </property>
            </widget>
           </item>
@@ -265,7 +265,7 @@
       </layout>
      </widget>
     </item>
-    <item row="3" column="0">
+    <item>
      <spacer name="verticalSpacer_3">
       <property name="orientation">
        <enum>Qt::Vertical</enum>
@@ -278,10 +278,10 @@
       </property>
      </spacer>
     </item>
-    <item row="4" column="0">
+    <item>
      <widget class="QGroupBox" name="nameGroupBox">
       <property name="title">
-       <string>Name</string>
+       <string>Run Name</string>
       </property>
       <layout class="QVBoxLayout" name="verticalLayout_4">
        <item>
@@ -307,7 +307,114 @@
       </layout>
      </widget>
     </item>
-    <item row="5" column="0">
+    <item>
+     <widget class="QGroupBox" name="analysisGroupBox">
+      <property name="title">
+       <string>Analysis</string>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout_7">
+       <item>
+        <layout class="QHBoxLayout" name="dutNameLayout">
+         <item>
+          <widget class="QLabel" name="dutNameLabel">
+           <property name="text">
+            <string>DUT name</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QLineEdit" name="dutName"/>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="analysisMiddleLayout">
+         <item>
+          <widget class="QGroupBox" name="analysisTypeGroupBox">
+           <property name="title">
+            <string>Analysis type</string>
+           </property>
+           <layout class="QVBoxLayout" name="verticalLayout_6">
+            <item>
+             <widget class="QRadioButton" name="analysisTypeDefaultButton">
+              <property name="text">
+               <string>Default</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="Line" name="line">
+              <property name="orientation">
+               <enum>Qt::Horizontal</enum>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QRadioButton" name="analysisTypeCableButton">
+              <property name="text">
+               <string>RF Cable</string>
+              </property>
+              <property name="checked">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <layout class="QHBoxLayout" name="cableLengthLayout">
+              <item>
+               <widget class="QLabel" name="cableLengthLabel">
+                <property name="text">
+                 <string>Length [m]</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QDoubleSpinBox" name="cableLengthSpinBox">
+                <property name="value">
+                 <double>10.000000000000000</double>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </item>
+           </layout>
+          </widget>
+         </item>
+         <item>
+          <widget class="QGroupBox" name="removeOffsetsGroupBox">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="title">
+            <string>Remove offsets</string>
+           </property>
+           <layout class="QVBoxLayout" name="verticalLayout_5">
+            <item>
+             <widget class="QCheckBox" name="removeOffsetsSignal1">
+              <property name="text">
+               <string>Signal 1</string>
+              </property>
+              <property name="checked">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QCheckBox" name="removeOffsetsSignal2">
+              <property name="text">
+               <string>Signal 2</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </item>
+    <item>
      <layout class="QHBoxLayout" name="startButtonLayout">
       <item>
        <spacer name="horizontalSpacer">
@@ -351,7 +458,7 @@
     <rect>
      <x>0</x>
      <y>0</y>
-     <width>618</width>
+     <width>536</width>
      <height>30</height>
     </rect>
    </property>
@@ -383,8 +490,8 @@
    <slot>setDisabled(bool)</slot>
    <hints>
     <hint type="sourcelabel">
-     <x>122</x>
-     <y>514</y>
+     <x>136</x>
+     <y>504</y>
     </hint>
     <hint type="destinationlabel">
      <x>138</x>
@@ -408,5 +515,37 @@
     </hint>
    </hints>
   </connection>
+  <connection>
+   <sender>analysisTypeCableButton</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>removeOffsetsGroupBox</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>139</x>
+     <y>719</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>60</x>
+     <y>557</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>analysisTypeCableButton</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>removeOffsetsSignal1</receiver>
+   <slot>setChecked(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>115</x>
+     <y>723</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>92</x>
+     <y>586</y>
+    </hint>
+   </hints>
+  </connection>
  </connections>
 </ui>
diff --git a/Python_script/climate_chamber.py b/Python_script/climate_chamber.py
index 3632c7d252c1548a52da7161aa9ade153d76d2ea..3512e434d410d6d710a324dd43cee781b0f9db64 100755
--- a/Python_script/climate_chamber.py
+++ b/Python_script/climate_chamber.py
@@ -247,6 +247,20 @@ class ClimateChamber:
         while (not self.is_humidity_reached()) or (not self.is_temperature_reached()):
             time.sleep(10)
             break
+    
+    def get_heater_percentage(self):
+        
+        response = self.chamber.query("%?", delay=MONITOR_COMMAND_DELAY)
+        
+        heater_param = response.split(sep=',')
+        heater_count = int(heater_param[0])
+        perc_temp_heater = float(heater_param[1])
+        perc_hum_heater = float(heater_param[2])
+        
+        return perc_temp_heater, perc_hum_heater
+            
+            
+            
 
     def __del__(self):
         self.close()
diff --git a/Python_script/climate_chamber_dummy.py b/Python_script/climate_chamber_dummy.py
index 3b62f3531224aa71e550d525263cb45ad58d9abc..e98f919fecc733297484e86603112b1c1201d2a9 100644
--- a/Python_script/climate_chamber_dummy.py
+++ b/Python_script/climate_chamber_dummy.py
@@ -161,3 +161,7 @@ class ClimateChamberDummy:
 
     def __del__(self):
         self.close()
+
+    def get_heater_percentage(self):
+        return 0.0, 0.0
+
diff --git a/Python_script/curvefit_and_correlation.py b/Python_script/curvefit_and_correlation.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ff057cc07c0c30aedac737c35cf2114dff36ada
--- /dev/null
+++ b/Python_script/curvefit_and_correlation.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Jul 27 08:53:36 2023
+
+@author: pawelzik
+"""
+import numpy as np
+from scipy.optimize import curve_fit
+from PostPlot import PostPlot
+import pandas as pd
+from pathlib import Path
+
+class reg_and_corr:
+
+    def __init__(self, trace_subplot5 = '', legend_loc = 'upper left', \
+                 legend_bbox_to_anchor = (1.09, 1)):
+        
+        self.postplot = PostPlot(trace_subplot5 = trace_subplot5)
+        self.K_phases = None
+        self.phase_t0 = None
+        self.legend_loc = legend_loc 
+        self.legend_bbox_to_anchor = legend_bbox_to_anchor
+
+
+    
+    def calc_correlation(self, func_1 , func_2 , state = 'False'):
+        
+        if state == True:
+        
+            # calulate correlation between two parameter from data frame
+            corr_coeff= np.corrcoef(func_1, func_2)[0][1]
+                
+            annotate_string_corr_coeff = "$R_{\phi(S_{21}),Temp_{DUT}}$ = %.3f" % corr_coeff
+        
+            self.postplot.edit_annotation_in_plot(annotate_string_corr_coeff)
+    
+    def calc_regression_coeff_phase_S21(self, data_frame, time_unit = 'min', state = False):
+            
+        if state == True:
+            phases = data_frame.S21_PHASE.tolist()
+            self.phase_t0 = phases[0] 
+    
+            self.K_phases = np.median(data_frame.S21_PHASE)
+            
+            # time stamp of index = 0 is the start point of time axis, Michael
+            # substract all other timestamps with time stamp of index zero to get time scale in
+            # seconds, Michael
+            
+            time_sec = data_frame.TIMESTAMP - data_frame.TIMESTAMP[0]
+            
+            # set scaling of time axis depending on the time unit given in parameter list, Michael
+            # default unit is minutes, Michael
+            time_vals = self.scaling_time_axes(time_sec, time_unit)
+            
+            func, popt = self.choose_fit(time_vals, data_frame.S21_PHASE)
+            
+            annotate_string_reg_coeff = self.plot_fitted_func_param(func, *popt, time_unit = time_unit)
+            
+            self.postplot.edit_annotation_in_plot(annotate_string_reg_coeff)
+        
+    
+
+    
+    def plot_phase_curve_fit(self, data_frame, time_unit = 'min', state = False):
+        
+        if state == True:
+            
+            
+            phases = data_frame.S21_PHASE.tolist()
+            self.phase_t0 = phases[0] 
+
+            self.K_phases = np.median(data_frame.S21_PHASE)
+            
+            # time stamp of index = 0 is the start point of time axis, Michael
+            # substract all other timestamps with time stamp of index zero to get time scale in
+            # seconds, Michael
+            
+            time_sec = data_frame.TIMESTAMP - data_frame.TIMESTAMP[0]
+            
+            # set scaling of time axis depending on the time unit given in parameter list, Michael
+            # default unit is minutes, Michael
+            time_vals = self.scaling_time_axes(time_sec, time_unit)
+            
+            # call chose fit function
+            func, popt = self.choose_fit(time_vals, data_frame.S21_PHASE)
+            
+            # genrate trace for curvefit
+            fit_phaeses = func(time_vals, *popt) 
+            
+            # determine max, min of measured phases
+            min_phases, max_phases = self.get_extended_min_max(data_frame.S21_PHASE)
+            
+            # determine max, min of curvefit
+            min_fit_phases, max_fit_phases = self.get_extended_min_max(fit_phaeses)
+            
+            # determine min of measure phases and min of curvefit 
+            minimum = min(min_phases, min_fit_phases)
+            
+            # determine max of measured phases and max of curvefit
+            maximum = max(max_phases, max_fit_phases)
+            
+            self.postplot.add_curvefit_to_plot(xvals =time_vals, \
+                                               yvals = func(time_vals, *popt), \
+                                               y_lim =[minimum, maximum],  \
+                                               trace_color = 'green', \
+                                               trace_label = 'fitted by\n' + func.__name__)
+            
+        else:
+            self.postplot.add_curvefit_to_plot(xvals =[], \
+                                   yvals = [], \
+                                   y_lim = None,  \
+                                   trace_color = 'None', \
+                                   trace_label = '')  
+
+
+
+# step response of PT1 with initial behavior , Michael    
+    def PT1(self, x, K, T1):
+        return K * (1 - np.exp(-x/T1)) + self.phase_t0 * np.exp(-x/T1)
+    
+    # step response of PT2 (D > 1) with initial behavior 
+    def aperiodicPT2(self, x, K, T1, T2,):
+        return K + self.phase_t0* np.exp(-x/T1)/(T2-T1) - K *T1* np.exp(-x/T1)/(T2-T1) \
+                    - self.phase_t0 * np.exp(-x/T2)/(T2-T1) + K* T2* np.exp(-x/T2)/(T2-T1)
+    
+    # step response of PT2 (D = 1) with initial behavior, Michael
+    def aper_borderPT2(self, x, K, T):
+        return K + self.phase_t0*np.exp(-x/T) - K * np.exp(-x/T) + self.phase_t0*np.exp(-x/T)*x/T + \
+            - K * np.exp(-x/T) * x/T
+
+    # step response of PT2 (0 < D < 1) with initial behavior, Michael 
+    def periodicPT2(self, x, D, T):
+        return self.K_phases + (self.phase_t0 -self.K_phases)* \
+            np.exp(-D*x/T)*np.cos(np.sqrt(1-np.square(D))*x/T) + \
+            (self.phase_t0 - self.K_phases) * D * \
+                np.exp(-D*x/T)*np.sin(np.sqrt(1-np.square(D))*x/T)/np.sqrt(1-np.square(D))
+        
+    # step response series of PT2 ( D = 1) and PT1 with initial behavior, Michael
+    def aper_borderPT2_PT1(self, x, a, b, c, d, T1, T2):
+        return a + b*np.exp(-x/T1)+c*x*np.exp(-x/T1) + d*np.exp(-x/T2)
+    
+    # method for cuvefit, Michael
+    # two different algorithms for curvefit are use depending on used step response for fit, Michael
+    def curvefit(self, func, time, phases):
+
+        if func.__name__ == 'periodicPT2':
+            popt, pcov = curve_fit(func, time, phases, method = 'trf', \
+                                   bounds = ([0.001, 0.001], [0.99, np.inf]))
+        else:    
+            popt, pcov = curve_fit(func, time, phases, method = 'lm')
+
+        return popt, pcov
+    
+    # method adds the parameter of the fit with the lowest error to annotation string, Michael
+
+    def plot_fitted_func_param(self, func, *popt, time_unit):
+
+        if func.__name__ == 'PT1':
+            K = popt[0] 
+            T1 = popt[1]
+            annotate_string = "K: %.2f\n$T_1$: %.3f %s" %(K, T1, time_unit) 
+        elif func.__name__ == 'aperiodicPT2':
+            K = popt[0]
+            T1 = popt[1]
+            T2 = popt[2]
+            annotate_string = "K: %.2f\n$T_1$: %.3f %s\n$T_2$: %.3f %s" % (K, T1, time_unit, T2, \
+                                                                           time_unit)
+        elif func.__name__ == 'aper_borderPT2':
+            K = popt[0]
+            T = popt[1]
+            annotate_string = "K: %.2f\nT: %.3f %s" %(K, T, time_unit) 
+        elif func.__name__ == 'periodicPT2':
+            K = self.K_phases
+            D = popt[0]
+            T = popt[1]
+            T_2perc = 4*T/D
+            annotate_string = "K: %.2f\nD: %.3f\nT: %.3f %s\n$T_E$(2%%): %.2f %s" \
+                % (K, D, T, time_unit, T_2perc, time_unit)
+        elif func.__name__ == 'aper_borderPT2_PT1':
+            T1 = popt[4]
+            T2 = popt[5]
+            annotate_string = "$T_1$: %.3f %s\n$T_2$: %.3f %s" % (T1, time_unit, T2, time_unit)
+        else:
+            # default value
+            annotate_string =''
+        return annotate_string
+    
+    # calculates the fit for all step responses and calulates the lowest error, Michael
+    # parameter of the plot with the lowest error is returned by this function, Michael
+    # if curve fit fails for one stept response it takes the next one and plots in command
+    # window the functions
+    # which failes, Michael
+    def choose_fit(self, time, phases):
+        popt = []
+        perr = []
+        used_functions = []
+        functions = [self.PT1, self.aper_borderPT2, self.aperiodicPT2, self.periodicPT2, \
+                     self.aper_borderPT2_PT1] 
+        for func in functions:
+            try:
+                # call curvefit method and put the fitted parameter for step response function
+                # to a list, Michael
+                popt.append(self.curvefit(func, time, phases)[0])
+                # calculation of fitting error
+                perr.append(np.sqrt(np.square(np.subtract(phases, func(time, *popt[-1]))).mean()))
+                used_functions.append(func)
+            except RuntimeError:
+                    print('curvefit of %s fails' %func.__name__)
+        # get position of parameters in list which has the lowest calculated error, Michael
+        pos = perr.index(min(perr))
+        # return function name and parameter with the lowest calculated error during curvefit, Michael
+        return used_functions[pos], popt[pos]
+
+    
+
+    # scaling time axes depending on the time unit
+    def scaling_time_axes(self, time_sec, time_unit):
+        if time_unit == 'min':
+            time_vals = time_sec/60
+        
+        elif time_unit == 'hours':
+            time_vals = time_sec/3600
+        
+        elif time_unit == 'sec':
+            time_vals = time_sec
+            
+        else:
+            time_vals = time_sec/60
+    
+        return time_vals
+    
+        # add 5 % of the distance between min and max to the range
+    @staticmethod
+    def get_extended_min_max(array):
+        distance = array.max() - array.min()
+        if distance == 0.:
+            distance = 1
+        return array.min()-0.05*distance, array.max()+0.05*distance
+
+
+# for manually redo post plot after measurement has finished and insert analysis function, Michael
+if __name__ == '__main__':
+    
+    
+    # set result path for post plot 
+    Results_Path = r'C:\git\climate-lab-test-stand\Python_script\TestData_JBY240'
+    
+    
+    # set time unit for post post plot
+    # default is minutes if entry in parameterlist left empty
+    # possible entries: 'min' for minutes, 'hours' for hours and 'sec' for seconds
+    time_unit = 'min'
+    
+    # set storepath for the post plots, Michael
+    storepath = Results_Path + '\\AnalysisPlots' 
+    
+    # search all csv files in results folder 
+    csv_file_list = list(Path(Results_Path).glob("**/*.csv"))
+    
+    # choose annotation option
+    plot_correlation_coeff = False
+    plot_regression_coeff = False
+    
+    # activate plot curvefitting
+    
+    plot_trace_curvefit = True
+    
+    # selction measurment data should be plotted in subplot5
+    # 'logger sens' : values for temperature and humidity of logger sensor in
+    # measurement instrument chmaber
+    # 'chamber_sens' : values for temepratere and humidity readback from chamber sensors
+    # of chamber for measurement instruments
+    # 'heater_dut_chamber': activity of temp heater and hum heater readback from DUT chamber
+    # This is also the default parameter
+    
+    trace_selection = ""
+    
+    
+    # create postplot object, Michael
+    analysisplot_obj = reg_and_corr(trace_subplot5= trace_selection)
+        
+    
+    # empty data frame for concat the data frames from csv import to plot full transistion
+    concat_data_frame = pd.DataFrame()
+    
+    # plot results for each csv-file
+    for index, csv_file in enumerate(csv_file_list):
+    
+        # import csv-data from csv-files in list, Michael
+        data_frame = analysisplot_obj.postplot.import_csv(str(csv_file))
+        
+        # determine title of plot from csv-filename, Michael
+        title = csv_file.name
+        
+        # concate datesframe for plotting full transistion data 
+        concat_data_frame = pd.concat([concat_data_frame,data_frame],ignore_index=True, sort = False)       
+        
+        
+        # plot frame data
+        analysisplot_obj.postplot.plot_frame_data(data_frame, title, time_unit) 
+        
+            
+        # determine correlation coefficient between func 1 and func 2 and plot coefficient
+        analysisplot_obj.calc_correlation(func_1 = data_frame.S21_PHASE , \
+                                  func_2 = data_frame.TEMP_DUT, state = plot_correlation_coeff )
+       
+     
+        
+            
+        # start curvefit and determine coefficients best fit function and plot coefficients
+        analysisplot_obj.calc_regression_coeff_phase_S21(data_frame, time_unit = 'min', \
+                                                      state = plot_regression_coeff)
+        
+            
+        analysisplot_obj.plot_phase_curve_fit(data_frame = data_frame, time_unit = time_unit, \
+                                          state = plot_trace_curvefit)
+        
+        # set filename of post plot, Michael
+        filename = str(csv_file.stem) + '.pdf'
+        
+        # store post plot under the path taht is set in storepath with the earlier defined filename
+        analysisplot_obj.postplot.save_fig(storepath, filename)
+    
+      
+    # plot of all steps of a sweep is plotted in one diagram, Michael
+    # title of this plot will be always full transistion, Michel
+    analysisplot_obj.postplot.plot_frame_data(concat_data_frame, 'Full Transistion', time_unit)
+    
+    analysisplot_obj.plot_phase_curve_fit(data_frame = concat_data_frame, time_unit = time_unit, \
+                                      state = False)
+    
+    # filename of plot that contains all steps of sweep is set to FullTransistion, Michael
+    filename = 'Full_Transistion' + '.pdf'
+    
+    # plot with the results of all steps is store under the predefined storpath with the 
+    # earlier defined filename, Michael
+    analysisplot_obj.postplot.save_fig(storepath, filename)    
+  
\ No newline at end of file
diff --git a/Python_script/delay_stage.map b/Python_script/delay_stage.map
new file mode 100644
index 0000000000000000000000000000000000000000..68daa88209fc68311d81df13247e5a4479fd82e9
--- /dev/null
+++ b/Python_script/delay_stage.map
@@ -0,0 +1,3 @@
+RB_ENCODER  1 0 4 0
+POSITION_SP 1 4 4 0
+SPEED       1 8 4 0
diff --git a/Python_script/deviceaccess_measurement.py b/Python_script/deviceaccess_measurement.py
new file mode 100644
index 0000000000000000000000000000000000000000..31493e72263713edf5b62089f1777578a7e61a7c
--- /dev/null
+++ b/Python_script/deviceaccess_measurement.py
@@ -0,0 +1,44 @@
+
+import dut_measurement
+import deviceaccess
+import numpy
+
+
+class DeviceAccessMeasurement(dut_measurement.DutMeasurement):
+    def __init__(self, config_data):
+
+        self.deltas = config_data['deltas']
+
+        deviceaccess.setDMapFilePath(config_data['dmap_file'])
+        self.device = deviceaccess.Device(config_data['device_alias'])
+        self.device.open()
+
+        self.reference_signal_names = config_data['reference_signals']
+
+        self.accessors = {}
+        for signal_name, register_path in config_data['signals'].items():
+            self.accessors[signal_name] = self.device.getScalarRegisterAccessor(numpy.double, register_path)
+
+    def get_dut_measurements(self, set_name):
+        retval = {}
+
+        for name, accessor in self.accessors.items():
+            accessor.read()
+            retval[name] = accessor[0]
+        retval['SET_NAME'] = 'default'
+        return retval
+
+    def get_dut_signal_names(self):
+        retval = list(self.accessors.keys())
+        retval.append('SET_NAME')
+        return retval
+
+
+    def get_dut_reference_signal_names(self):
+        return self.reference_signal_names
+
+    def get_dut_max_delta_signals(self):
+        return self.deltas
+
+    def get_measurement_set_names(self):
+        return ['default']
diff --git a/Python_script/devices.dmap b/Python_script/devices.dmap
new file mode 100644
index 0000000000000000000000000000000000000000..7ebc8687f439098c472be133e063419448b8c7f1
--- /dev/null
+++ b/Python_script/devices.dmap
@@ -0,0 +1,3 @@
+@LOAD_LIB /usr/lib/x86_64-linux-gnu/libChimeraTK-DeviceAccess-DoocsBackend.so
+delay_stage (sharedMemoryDummy?map=delay_stage.map)
+#delay_stage (doocs?/MY_FACILITY/MY_DEVICE/MY_LOCATION)
diff --git a/Python_script/dut_measurement.py b/Python_script/dut_measurement.py
new file mode 100644
index 0000000000000000000000000000000000000000..b2f217138468f4fe203e1c9d123e9fb24db3a5ec
--- /dev/null
+++ b/Python_script/dut_measurement.py
@@ -0,0 +1,41 @@
+from abc import ABC, abstractmethod
+
+
+class DutMeasurement(ABC):
+    """
+    Interface for DUT measurements
+    """
+    @abstractmethod
+    def get_dut_measurements(self, set_name):
+        """
+        Performs the measurements according to the set name and
+        returns a dictionary with signal names a keys and scalar values
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_signal_names(self):
+        """
+        Returns a list of names used as keys used in the dut measurements dictionary
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_reference_signal_names(self):
+        """
+        Returns a list of signal names used for stability checks
+        """
+        pass
+
+    @abstractmethod
+    def get_dut_max_delta_signals(self):
+        """
+        Return the maximum deltas of the reference signals
+        """
+
+    @abstractmethod
+    def get_measurement_set_names(self):
+        """
+        Names /  string identifiers of the different measurement sets which are performed at each temperature/humidity
+        """
+        pass
diff --git a/Python_script/ext_sensor_channels.json b/Python_script/ext_sensor_channels.json
new file mode 100644
index 0000000000000000000000000000000000000000..107a681561d5756a52d863f028a34ef7e76fda23
--- /dev/null
+++ b/Python_script/ext_sensor_channels.json
@@ -0,0 +1 @@
+{"temp_dut": "0.0", "hum_dut": "0.1", "temp_room": "1.0" , "hum_room": "1.1", "air_press_room":  "1.3", "temp_meas_instr": "2.0", "hum_meas_instr": "2.1"}
diff --git a/Python_script/external_sensors.py b/Python_script/external_sensors.py
new file mode 100644
index 0000000000000000000000000000000000000000..b73b0a43e6c42b40c2fd1cc674e5537f8d753f74
--- /dev/null
+++ b/Python_script/external_sensors.py
@@ -0,0 +1,79 @@
+from almemo710 import almemo710
+from almemo2490 import almemo2490
+
+import pandas as pd
+import climate_chamber_dummy
+
+
+class ExternalSensorsDummy:
+    def __init__(self):
+        self.climate_chamber_dummy = climate_chamber_dummy.get_climate_chamber_dummy([0.2, 0.1])
+
+    def get_sensor_values(self):
+        # FIXME: Do we have to run the simulation here or do we piggy-back on the chamber probably also being dummy?
+        temp = self.climate_chamber_dummy.read_temperature()[0]
+        humidity = self.climate_chamber_dummy.read_humidity()[0]
+        # half a degree thermal cycle of 10 minutes from the room's air conditioning
+        room_temp = 22.97 + abs((self.climate_chamber_dummy.last_simulation_time % 600)-300)/600
+        # Use the same DUT chamber values as the simulation, but constant measurement instrument chamber and room
+        # temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room
+        return temp, room_temp, 23.0, humidity, 62.2, 45.0, 1013
+
+    def close(self):
+        pass
+
+
+def create_sensors(logger_model, logger_address, ext_sensor_channels):
+    if logger_address == 'localhost':
+        return ExternalSensorsDummy()
+    else:
+        return ExternalSensors(logger_model, logger_address, ext_sensor_channels)
+
+
+class ExternalSensors:
+    def __init__(self, logger_model, logger_address, ext_sensor_channels):
+        if logger_model == '710':
+            self.logger = almemo710(ip=logger_address, timeout=10)
+        elif logger_model == '2490':
+            self.logger = almemo2490(ip=logger_address, timeout=10)
+        else:
+            raise Exception('Unknown logger type: '+logger_model)
+
+        self.logger_model = logger_model
+        self.ext_sensor_channels = ext_sensor_channels
+        # initialise the logger
+        self.logger.request_sens_channel_list()
+
+    def get_sensor_values(self):
+        """
+        Returns: (temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room)
+        """
+        sens_vals_dict = dict()
+        self.logger.request_meas_vals_all_channels()
+
+        for key, value in self.ext_sensor_channels.items():
+            sens_vals_dict[key] = self.logger.fetch_channel_param_from_meas_buffer(pattern=value)[0].meas_val
+
+        # set values from series to variables
+        sens_vals_series = pd.Series(sens_vals_dict)
+
+        # set series items for sensor value to matching variable
+        temp_dut = sens_vals_series.temp_dut
+        temp_room = sens_vals_series.temp_room
+        hum_dut = sens_vals_series.hum_dut
+        hum_room = sens_vals_series.hum_room
+        air_press_room = sens_vals_series.air_press_room
+
+        if self.logger_model == '710':
+            temp_meas_instr = sens_vals_series.temp_meas_instr
+            hum_meas_instr = sens_vals_series.hum_meas_instr
+
+        else:
+            temp_meas_instr = -30
+            hum_meas_instr = 0
+
+        return temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room
+
+    def close(self):
+        self.logger.close()
+
diff --git a/Python_script/first_tempsweep.txt b/Python_script/first_tempsweep.txt
index a9dfacdb85f2b2fbbd6ab0892455e61ab9031ac7..fc6d2565583ca39f4fa952043e9ac1fbb1ad6886 100644
--- a/Python_script/first_tempsweep.txt
+++ b/Python_script/first_tempsweep.txt
@@ -1,2 +1,2 @@
-25.1 35.1 2.5 35 300 30
+20 31 1 35 100 10
 
diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..242ecc3ec97feeeda571032569746e05eee9a1fd
Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_25.0deg_55.0rh.pdf differ
diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..d5d310f82b79d2dff7b41a68060cbf094ef1e05d
Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/2023_07_31-16_56_02_results_30.0deg_55.0rh.pdf differ
diff --git a/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..1de26f13c30314d4ca0d09e9983587c84a664321
Binary files /dev/null and b/Python_script/measurements/2023_07_31-16_56_02_results/PostPlots/FullTransistion.pdf differ
diff --git a/Python_script/play_with_almemo2490.py b/Python_script/play_with_almemo2490.py
new file mode 100644
index 0000000000000000000000000000000000000000..fecf80911ac46a7a56d696eebf6807bc6df218d4
--- /dev/null
+++ b/Python_script/play_with_almemo2490.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Jan  2 16:55:57 2023
+
+@author: michael pawelzik
+"""
+
+# import module with python class for ahlborn
+import almemo2490
+
+# variables for selection in used methods
+# numbmer format: 'xy'  where x has to be [0-9] and y has be [0-1]
+# x is the measurement channel and y is the sensor port
+sel_channel_no = '10'
+sel_channel_name = 'T,t'
+item = 0
+
+try:
+    
+    # create object for python class of almemo710
+    almemo2490_obj = almemo2490.almemo2490(ip='192.168.115.44')
+
+    # set date to catual date
+    almemo2490_obj.set_date()
+    
+    #set time to actual time
+    almemo2490_obj.set_time()
+    
+    # request date from almemo 710  
+    date = almemo2490_obj.get_date()
+    
+    # request time from almemo 710
+    time = almemo2490_obj.get_time()
+    
+    # set name of measurment channel 0.1 from almemo 710  
+    almemo2490_obj.set_channel_name(sel_channel_no,sel_channel_name)
+    
+    # request name of measurement channel from almemo 710
+    channel_name = almemo2490_obj.get_channel_name(sel_channel_no)
+        
+    # request list of all sensors channels that are currently active
+    # store this data in data frame of class_object
+    almemo2490_obj.request_sens_channel_list()
+ 
+    # trigger single measurement an request the measured values for all measurment channels
+    # of almemo 710
+    # store measured values in data frame of python class
+    almemo2490_obj.request_meas_vals_all_channels()
+    
+    # get measuerement buffer of class object
+    meas_buffer = almemo2490_obj.meas_buffer
+    
+    # fetch parameter of selected measurement channel from meas_buffer of class object
+    # input of channel name or channel number is possible
+    # numbmer format: 'xy'  where x has to be [0-9] and y has be [0-1]
+    meas_channel_data, num_matches = almemo2490_obj. \
+        fetch_channel_param_from_meas_buffer(pattern = sel_channel_name, index = item)
+    
+    # get measurement date from selected measurement channel
+    meas_date = meas_channel_data.meas_date
+    
+    # get measurement time from selected measurement channel
+    meas_time = meas_channel_data.meas_time
+    
+    # get measurement value from selected measurement channel
+    meas_value = meas_channel_data.meas_val
+    
+    # get sensor unit from selected measurement channel
+    channel_unit = meas_channel_data.channel_unit
+    
+    # get channel name from selected measurement channel
+    channel_name = meas_channel_data.channel_name
+    
+    # get channel number from selected measurement channel
+    sens_channel = meas_channel_data.sens_channel
+
+finally:
+    
+    # close telnet connection
+    almemo2490_obj.close()
+    
diff --git a/Python_script/play_with_almemo710.py b/Python_script/play_with_almemo710.py
new file mode 100644
index 0000000000000000000000000000000000000000..79c88760e86aecfe312adc66d682e804c5aa79f5
--- /dev/null
+++ b/Python_script/play_with_almemo710.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Jan  2 16:55:57 2023
+
+@author: michael pawelzik
+"""
+
+# import module with python class for ahlborn
+import almemo710
+
+# variables for selection in used methods
+# numbmer format: 'x.y'  where x has to be [0-9] and y has be [0-9]
+# x is the sensor port and y is the measurement channel
+sel_channel_no = '0.1'
+sel_channel_name = 'T,t'
+item = 0
+
+try:
+    
+    # create object for python class of almemo710
+    almemo710_obj = almemo710.almemo710(ip='192.168.115.94')
+    
+    # set date to catual date
+    almemo710_obj.set_date()
+    
+    #set time to actual time
+    almemo710_obj.set_time()
+    
+    # request date from almemo 710  
+    date = almemo710_obj.get_date()
+    
+    # request time from almemo 710
+    time = almemo710_obj.get_time()
+    
+    # set name of measurment channel 0.1 from almemo 710  
+    almemo710_obj.set_channel_name(sel_channel_no,sel_channel_name)
+    
+    # request name of measurement channel from almemo 710
+    channel_name = almemo710_obj.get_channel_name(sel_channel_no)
+        
+    # request list of all sensors channels that are currently active
+    # store this data in data frame of class_object
+    almemo710_obj.request_sens_channel_list()
+     
+    # trigger single measurement an request the measured values for all measurment channels
+    # of almemo 710
+    # store measured values in data frame of python class
+    almemo710_obj.request_meas_vals_all_channels()
+    
+    # get measuerement buffer of class object
+    meas_buffer = almemo710_obj.meas_buffer
+    
+    # fetch parameter of selected measurement channel from meas_buffer of class object
+    # input of channel name or channel number is possible
+    # numbmer format: 'x.x' where is has to be [0-9]
+    meas_channel_data, num_matches = almemo710_obj. \
+        fetch_channel_param_from_meas_buffer(pattern = sel_channel_name, index = item)
+    
+    # get measurement date from selected measurement channel
+    meas_date = meas_channel_data.meas_date
+    
+    # get measurement time from selected measurement channel
+    meas_time = meas_channel_data.meas_time
+    
+    # get measurement value from selected measurement channel
+    meas_value = meas_channel_data.meas_val
+    
+    # get sensor unit from selected measurement channel
+    channel_unit = meas_channel_data.channel_unit
+    
+    # get channel name from selected measurement channel
+    channel_name = meas_channel_data.channel_name
+    
+    # get channel number from selected measurement channel
+    sens_channel = meas_channel_data.sens_channel
+
+finally:
+    
+    # close telnet connection
+    almemo710_obj.close()
+    
diff --git a/Python_script/prototype.py b/Python_script/prototype.py
index cc7a055f9220c13e7aadee5e5b709aa0b3127650..cc3a5280c0bdad75fbe1132b46ffae0de5a15c19 100755
--- a/Python_script/prototype.py
+++ b/Python_script/prototype.py
@@ -1,70 +1,99 @@
 #!/usr/bin/python3
 import csv
-import math
-import cmath
 import time
 import numpy
 from argparse import ArgumentParser
 import pandas as pd
 import matplotlib.pyplot as plt
 import climate_chamber
-import VNA
+import vna_measurement
+import deviceaccess_measurement
 import virtual_time
 import json
 import MeasurementPlot
 import sys
 import analysis
 import threading
+import external_sensors
+import PostPlot
+import os
 
+# Only use these when calculating the equilibrium indicator. Don't use in the algorithm.
 TEMPERATURE_STABLE = 0x1
 HUMIDITY_STABLE = 0x2
-MAGNITUDE_STABLE = 0x4
-PHASE_STABLE = 0x8
+DUT_SIGNAL0_STABLE = 0x4
+DUT_SIGNAL1_STABLE = 0x8
 MEASUREMENT_STABLE = 0x10
 
 
-# Class has only attributes which we are using in the read_data_function to read data from VNA and Chamber
+# Class has only attributes which we are using in the read_data_function to read data from DUT and Chamber
 class MeasurementData:
-    def __init__(self, timestamp, temp, hum, power, frequency, s11, s21, s12, s22):
+    
+    def __init__(self, timestamp, temp, hum, dut_data, percent_temp_heater,
+                 percent_hum_heater, temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room,
+                 hum_meas_instr, air_press_room, temp_chamber_meas_instr, hum_chamber_meas_instr):
         self.timestamp = timestamp
         self.temp = temp
         self.hum = hum
-        self.power = power
-        self.frequency = frequency
-        self.s11 = s11
-        self.s21 = s21
-        self.s12 = s12
-        self.s22 = s22
+        self.dut_data = dut_data
+
+        self.percent_temp_heater = percent_temp_heater
+        self.percent_hum_heater = percent_hum_heater
+        self.temp_dut = temp_dut
+        self.temp_room = temp_room
+        self.temp_meas_instr = temp_meas_instr
+        self.temp_chamber_meas_instr=temp_chamber_meas_instr
+        self.hum_dut = hum_dut
+        self.hum_room = hum_room
+        self.air_press_room = air_press_room
+        self.hum_meas_instr = hum_meas_instr
+        self.hum_chamber_meas_instr = hum_chamber_meas_instr
 
 
 class Measurements:
-    def __init__(self, chamber_address, vna_address, output_basename, standby, config_data):
+    def __init__(self, config_data, output_basename, standby, ext_sensor_channels):
+        #create all members that need to be closed to be able to write a close function
+        #which does not throw because of unknown variables
+        self.chamber = None
+        self.instr_chamber = None
+        self.dut = None
+        self.ext_sensors = None
+        
         self.max_delta_temp = config_data['delta_temp']
         self.max_delta_hum = config_data['delta_hum']
-        self.max_delta_mag = config_data['delta_mag']
-        self.max_delta_phase = config_data['delta_phase']
+
+        self.ext_sensor_channels = ext_sensor_channels
         self.sleep_time = config_data["sleep_time"]
-        self.frequency = config_data["frequency"]
-        self.vna_config_file = config_data["vna_config_file"]
         target_accuracy = [self.max_delta_temp, self.max_delta_hum]
-        self.chamber = climate_chamber.create_chamber(chamber_address, target_accuracy)
-        self.vna = VNA.create_vna(vna_address, target_accuracy)
+        self.chamber = climate_chamber.create_chamber(config_data['chamber_ip'], target_accuracy)
+        self.instr_chamber = climate_chamber.create_chamber(config_data['instr_chamber_ip'],
+                                                            target_accuracy)
+        # FIXME: Do we want a factory function for the DUTs?
+        if config_data['dut']['type'] == 'VNA':
+            self.dut = vna_measurement.VnaMeasurement(config_data['dut'], target_accuracy)
+        elif config_data['dut']['type'] == 'DeviceAccess':
+            self.dut = deviceaccess_measurement.DeviceAccessMeasurement(config_data['dut'])
+        else:
+            raise Exception('Unknown DUT type: '+config_data['dut']['type'])
+
+        self.max_delta_dut_signals = self.dut.get_dut_max_delta_signals()
+
+        # data logger for external sensors
+        self.ext_sensors = external_sensors.create_sensors(config_data['logger_model'], config_data['logger_ip'],
+                                                           ext_sensor_channels)
+
         self.standby = standby
         self.output_basename = output_basename
-        self.clock = virtual_time.get_clock(chamber_address, target_accuracy)
+        self.clock = virtual_time.get_clock(config_data['chamber_ip'], target_accuracy)
         self.temperature_stable = False
         self.humidity_stable = False
-        self.magnitude_stable = False
-        self.phase_stable = False
-
-        self.vna.load_config(self.vna_config_file, self.frequency)
-        self.vna.create_new_trace("Trace1", "S11")
-        self.vna.create_new_trace("Trace2", "S12")
-        self.vna.create_new_trace("Trace3", "S21")
-        self.vna.create_new_trace("Trace4", "S22")
+        self.dut_signals_stable = [False, False]
 
-        self.measurement_plot = MeasurementPlot.MeasurementPlot()
-        self.data_collection = []
+        self.postplot_obj = None
+        
+        self.measurement_plot = MeasurementPlot.MeasurementPlot(self.dut.get_dut_reference_signal_names(),
+                                                                trace_subplot5=config_data['trace_subplot5'])
+        self.data_collection_for_online_plot = []
 
     def perform_measurements(self, sweep_file):
         with open(sweep_file) as file:
@@ -79,15 +108,16 @@ class Measurements:
 
                     self.measurement_plot.fig.suptitle("Measurement "+str(measurement_number)+': ' + str(next_temp) +
                                                        ' degC, ' + str(next_hum) + ' rel. hum.', color="red")
-                    self.perform_single_measurement(self.output_basename+'_'+str(measurement_number)+'.csv', next_temp, next_hum,
-                                                    next_soaking, next_reads)
+                    self.perform_single_measurement(self.output_basename+'_'+str(measurement_number)+'.csv', next_temp,
+                                                    next_hum, next_soaking, next_reads)
                     measurement_number += 1
 
             except KeyboardInterrupt:
                 pass
             except MeasurementPlot.PlottingError:
                 # Just the plotting failed, probably because the window was closed
-                # The measurement was partly done and should be plottet, so the following code has to see the correct number of successful measurements
+                # The measurement was partly done and should be plotted, so the following code has to see the correct
+                # number of successful measurements
                 measurement_number += 1
 
         plt.close()
@@ -130,10 +160,8 @@ class Measurements:
             pass
         except MeasurementPlot.PlottingError:
             # Remove the remaining measurements from the list.
-            # One measurement was partly done and should be plottet, so we leave 'val' in the list
-            del sweep_values[sweep_values.index(val)+1 : ]
-
-
+            # One measurement was partly done and should be plotted, so we leave 'val' in the list
+            del sweep_values[sweep_values.index(val)+1:]
 
         plt.close()
         return sweep_values
@@ -141,6 +169,11 @@ class Measurements:
     # wrapper function which calls the impl in a separate thread and runs the
     # plotting loop
     def perform_single_measurement(self, output, target_temp, target_hum, soaking_time, n_stable_reads):
+        """
+        A "single measurement refers to a single temperature and humidity setting.
+        This is a "chamber measurement point". It consists out of multiple measurements sets. The data for all
+        measurement sets of this chamber point are taken for this measurement.
+        """
         measurement_thread = threading.Thread(target=self.perform_single_measurement_impl,
                                               args=(output, target_temp, target_hum, soaking_time,
                                                     n_stable_reads))
@@ -151,13 +184,15 @@ class Measurements:
     def perform_single_measurement_impl(self, output, target_temp, target_hum, soaking_time, n_stable_reads):
         with open(output, mode='w', newline='') as csv_file:
             fieldnames = ['TIMESTAMP', 'TARGET_TEMPERATURE', 'READBACK_TEMPERATURE', 'TARGET_HUMIDITY',
-                          'READBACK_HUMIDITY', 'RF_POWER', 'RF_FREQUENCY', 'DUT_IDENTIFIER', 'RUN_ID',
-                          'EQUILIBRIUM_INDICATOR', 'S11_MAGNITUDE', 'S11_PHASE', 'S12_MAGNITUDE', 'S12_PHASE',
-                          'S21_MAGNITUDE', 'S21_PHASE', 'S22_MAGNITUDE', 'S22_PHASE']
+                          'READBACK_HUMIDITY', 'DUT_IDENTIFIER', 'RUN_ID', 'EQUILIBRIUM_INDICATOR', 'TEMP_HEATER',
+                          'HUM_HEATER', 'TEMP_DUT', 'TEMP_ROOM', 'TEMP_MEAS_INSTR', 'HUM_DUT', 'HUM_ROOM',
+                          'HUM_MEAS_INSTR', 'AIR_PRESS_ROOM', 'READBACK_TEMP_MEAS_INSTR', 'READBACK_HUM_MEAS_INSTR']
+            fieldnames.extend(self.dut.get_dut_signal_names())
+
             # csv.dict writer add adda row wise
             writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
             writer.writeheader()
-            self.data_collection = []
+            self.data_collection_for_online_plot = []
             plt.ion()
 
             set_const_response = self.chamber.set_const((target_temp, target_hum))
@@ -168,16 +203,17 @@ class Measurements:
             number_of_soaking_reads = soaking_time / self.sleep_time + 1
 
             do_another_measurement = True
-            #next_read_time is the starttime of the second read (i.e. read after this one). The time of
-            #this read (i.e. the first read in the measurement) is now().
+            # next_read_time is the starttime of the second read (i.e. read after this one). The time of
+            # this read (i.e. the first read in the measurement) is now().
             next_read_time = self.clock.time() + self.sleep_time
             while do_another_measurement:
                 # wait until set point is reached (+soaking time)
-                magnitudes_queue = []
-                phase_queue = []
+                dut_signal_queues = [[], []]
 
                 while True:
-                    data = self.read_data()
+                    # Only read the climate chamber data once. The equilibrium indicator is only calculated on the
+                    # primary data set.
+                    data = self.read_data(self.dut.get_measurement_set_names()[0])
                     # if it is not within the target range reset start time
                     self.temperature_stable = False
                     self.humidity_stable = False
@@ -185,45 +221,41 @@ class Measurements:
                     self.temperature_stable = self.calculate_temperature_stability(target_temp, float(data.temp))
                     self.humidity_stable = self.calculate_humidity_stability(target_hum, float(data.hum))
 
-                    # The queue must not be longer than the max number of soaking reads.
-                    # If the queue is already full, we have to pop the first element before we can add the
-                    # current measurement.
-                    if len(magnitudes_queue) >= number_of_soaking_reads:
-                        magnitudes_queue.pop(0)
-                    if self.temperature_stable and self.humidity_stable:
-                        magnitudes_queue.append(self.calculate_mean_magnitude(data.s21))
-                    else:
-                        magnitudes_queue.clear()
-                    # check cable stability parameters
-                    self.magnitude_stable = False
-                    if len(magnitudes_queue) >= number_of_soaking_reads:
-                        spread = max(magnitudes_queue) - min(magnitudes_queue)
-                        if spread < 2*self.max_delta_mag:
-                            self.magnitude_stable = True
-
-                    if len(phase_queue) >= number_of_soaking_reads:
-                        phase_queue.pop(0)
-                    if self.temperature_stable and self.humidity_stable:
-                        phase_queue.append(self.calculate_mean_phase(data.s21))
-                    else:
-                        phase_queue.clear()
-
-                    self.phase_stable = False
-                    if len(phase_queue) >= number_of_soaking_reads:
-                        spread = max(phase_queue) - min(phase_queue)
-                        if spread < 2*self.max_delta_phase:
-                            self.phase_stable = True
+                    # Use indexed loop because we need to modify, and zip seems to copy the content :-(
+                    for i, signal_queue in enumerate(dut_signal_queues):
+                        # The queue must not be longer than the max number of soaking reads.
+                        # If the queue is already full, we have to pop the first element before we can add the
+                        # current measurement.
+                        if len(signal_queue) >= number_of_soaking_reads:
+                            signal_queue.pop(0)
+                        if self.temperature_stable and self.humidity_stable:
+                            signal_queue.append(data.dut_data[self.dut.get_dut_reference_signal_names()[i]])
+                        else:
+                            signal_queue.clear()
+
+                        self.dut_signals_stable[i] = False
+                        if len(signal_queue) >= number_of_soaking_reads:
+                            spread = max(signal_queue) - min(signal_queue)
+                            if spread < 2*self.max_delta_dut_signals[i]:
+                                self.dut_signals_stable[i] = True
 
                     print('Setpoint: ' + str(target_temp) + ' ' + str(target_hum) + ' | Temp: ' + data.temp +
-                          ' °C' + ' | Humid: ' + data.hum + '%'
-                          + ' | soaking read nr' + str(len(magnitudes_queue)))
+                          ' °C' + ' | Humid: ' + data.hum + '%'
+                          + ' | soaking read nr' + str(len(dut_signal_queues[0])))
                     self.store_and_plot_data(target_temp, target_hum, data, self.cook_up_equi_indicator())
-                    writer.writerow(self.data_collection[-1])
-
-                    if self.temperature_stable and self.humidity_stable and self.magnitude_stable and\
-                            self.phase_stable:
-                        reference_magnitude = magnitudes_queue[-1]
-                        reference_phase = phase_queue[-1]
+                    writer.writerow(self.data_collection_for_online_plot[-1])
+
+                    # read and store data for all further sets
+                    for set_name in self.dut.get_measurement_set_names()[1:]:
+                        data.dut_data = self.dut.get_dut_measurements(set_name)
+                        writer.writerow(self.create_data_dictionary(target_temp, target_hum, data,
+                                                                    self.cook_up_equi_indicator()))
+
+                    if self.temperature_stable and self.humidity_stable and self.dut_signals_stable[0] and\
+                            self.dut_signals_stable[1]:
+                        reference_values = []
+                        for signal_queue in dut_signal_queues:
+                            reference_values.append(signal_queue[-1])
                         print('SOAKING FINISHED!')
                         break
                     else:
@@ -232,24 +264,35 @@ class Measurements:
 
                 # perform the configured number of measurements and check that they are really stable
                 # It started running after everything become stable
-                supposedly_stable_measurements = []
+                supposedly_stable_measurements = {}
+                for set_name in self.dut.get_measurement_set_names():
+                    supposedly_stable_measurements[set_name] = []
                 all_measurements_stable = True
+
                 for i in range(0, n_stable_reads):
-                    data = self.read_data()
+                    # create the main data set for the first measurement set
+                    data = self.read_data(self.dut.get_measurement_set_names()[0])
                     self.temperature_stable = self.calculate_temperature_stability(target_temp, float(data.temp))
                     self.humidity_stable = self.calculate_humidity_stability(target_hum, float(data.hum))
-                    mag = self.calculate_mean_magnitude(data.s21)
-                    phase = self.calculate_mean_phase(data.s21)
-                    self.magnitude_stable = (reference_magnitude-self.max_delta_mag <= mag) and\
-                                            (mag <= reference_magnitude+self.max_delta_mag)
-                    self.phase_stable = (reference_phase-self.max_delta_phase <= phase) and\
-                                        (phase <= reference_phase+self.max_delta_phase)
+                    this_measurement_stable = (self.temperature_stable and self.humidity_stable)
+                    for j, max_delta in enumerate(self.max_delta_dut_signals):
+                        value = data.dut_data[self.dut.get_dut_reference_signal_names()[j]]
+                        self.dut_signals_stable[j] = (reference_values[j] - max_delta <= value) and\
+                            (value <= reference_values[j] + max_delta)
+                        if not self.dut_signals_stable[j]:
+                            this_measurement_stable = False
 
                     self.store_and_plot_data(target_temp, target_hum, data, self.cook_up_equi_indicator())
-                    supposedly_stable_measurements.append(self.data_collection[-1])
+                    supposedly_stable_measurements[self.dut.get_measurement_set_names()[0]].append(
+                        self.data_collection_for_online_plot[-1])
+
+                    # read and store data for all further sets
+                    for set_name in self.dut.get_measurement_set_names()[1:]:
+                        data.dut_data = self.dut.get_dut_measurements(set_name)
+                        supposedly_stable_measurements[set_name].append(
+                            self.create_data_dictionary(target_temp, target_hum, data, self.cook_up_equi_indicator()))
                                 
-                    if (self.temperature_stable and self.humidity_stable and self.magnitude_stable and
-                            self.phase_stable):
+                    if this_measurement_stable:
                         print('Stable measurement ' + str(i+1) + '/' + str(n_stable_reads))
                         self.sleep_until(next_read_time)
                         next_read_time += self.sleep_time
@@ -258,14 +301,15 @@ class Measurements:
                         print('Measurement not stable. Retrying.')
                         break
 
-                for measurement in supposedly_stable_measurements:
-                    if all_measurements_stable:
-                        measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE |\
-                                                               MAGNITUDE_STABLE | PHASE_STABLE |\
-                                                               MEASUREMENT_STABLE
-                        do_another_measurement = False
+                for set_name in self.dut.get_measurement_set_names():
+                    for measurement in supposedly_stable_measurements[set_name]:
+                        if all_measurements_stable:
+                            measurement['EQUILIBRIUM_INDICATOR'] = TEMPERATURE_STABLE | HUMIDITY_STABLE | \
+                                                                   DUT_SIGNAL0_STABLE | DUT_SIGNAL1_STABLE | \
+                                                                   MEASUREMENT_STABLE
+                            do_another_measurement = False
 
-                    writer.writerow(measurement)
+                        writer.writerow(measurement)
 
             self.measurement_plot.stop()
 
@@ -274,73 +318,58 @@ class Measurements:
         if remaining_sleep_time > 0:
             self.clock.sleep(remaining_sleep_time)
 
-    def read_data(self):
+    def read_data(self, measurement_set_name):
         [temp, hum, mode, alarms] = self.chamber.read_monitor().split(',')
-        power = self.vna.get_current_power()
-        frequency = self.vna.get_current_cw_frequency()
-        self.vna.do_single_sweep()
-        s11 = self.get_trace_data("Trace1")
-        s12 = self.get_trace_data("Trace2")
-        s21 = self.get_trace_data("Trace3")
-        s22 = self.get_trace_data("Trace4")
+        perc_temp_heater, perc_hum_heater = self.chamber.get_heater_percentage()
 
-        return MeasurementData(int(self.clock.time()), temp, hum, power, frequency, s11, s21, s12, s22)
+        dut_data = self.dut.get_dut_measurements(measurement_set_name)
 
-    def store_and_plot_data(self, target_temp, target_hum, data, equi_indicator):
+        temp_dut, temp_room, temp_meas_instr, hum_dut, hum_room, hum_meas_instr, air_press_room = \
+            self.ext_sensors.get_sensor_values()
+        
+        [temp_chamber_meas_instr, hum_chamber_meas_instr, mode_meas_instr, alarms_meas_instr] = \
+            self.instr_chamber.read_monitor().split(',')
+
+        return MeasurementData(int(self.clock.time()), temp, hum, dut_data,
+                               perc_temp_heater, perc_hum_heater, temp_dut, temp_room, temp_meas_instr,
+                               hum_dut, hum_room, hum_meas_instr, air_press_room, temp_chamber_meas_instr,
+                               hum_chamber_meas_instr)
+
+    def create_data_dictionary(self, target_temp, target_hum, data, equi_indicator):
+        """
+        Create a dictionary out of the  MeasurementData object and some extra data, as needed by the writer
+        """
         measurement = {
             'TIMESTAMP': data.timestamp,
             'TARGET_TEMPERATURE': target_temp,
             'READBACK_TEMPERATURE': float(data.temp),
             'TARGET_HUMIDITY': target_hum,
             'READBACK_HUMIDITY': float(data.hum),
-            'RF_POWER': data.power,
-            'RF_FREQUENCY': data.frequency,
             'EQUILIBRIUM_INDICATOR': equi_indicator,
-            'S11_PHASE': self.calculate_mean_phase(data.s11),
-            'S11_MAGNITUDE': self.calculate_mean_magnitude(data.s11),
-            'S21_PHASE': self.calculate_mean_phase(data.s21),
-            'S21_MAGNITUDE': self.calculate_mean_magnitude(data.s21),
-            'S12_PHASE': self.calculate_mean_phase(data.s12),
-            'S12_MAGNITUDE': self.calculate_mean_magnitude(data.s12),
-            'S22_PHASE': self.calculate_mean_phase(data.s22),
-            'S22_MAGNITUDE': self.calculate_mean_magnitude(data.s22)
+            'TEMP_HEATER': data.percent_temp_heater,
+            'HUM_HEATER': data.percent_hum_heater,
+            'TEMP_DUT': data.temp_dut,
+            'TEMP_ROOM': data.temp_room,
+            'TEMP_MEAS_INSTR': data.temp_meas_instr,
+            'HUM_DUT': data.hum_dut,
+            'HUM_ROOM': data.hum_room,
+            'HUM_MEAS_INSTR': data.hum_meas_instr,
+            'AIR_PRESS_ROOM': data.air_press_room,
+            'READBACK_TEMP_MEAS_INSTR': float(data.temp_chamber_meas_instr),
+            'READBACK_HUM_MEAS_INSTR': float(data.hum_chamber_meas_instr),
         }
-        self.data_collection.append(measurement)
-        data_frame = pd.DataFrame(self.data_collection)
+        measurement.update(data.dut_data)
+        return measurement
+
+    def store_and_plot_data(self, target_temp, target_hum, data, equi_indicator):
+        measurement = self.create_data_dictionary(target_temp, target_hum, data, equi_indicator)
+        self.data_collection_for_online_plot.append(measurement)
+        data_frame = pd.DataFrame(self.data_collection_for_online_plot)
         self.measurement_plot.draw_in_other_thread(data_frame)
 
     def current_milli_time(self):
         return int(round(self.clock.time() * 1000))
 
-    def get_trace_data(self, trace):
-        return self.vna.get_list_of_measurement_values(trace, "SDAT")
-
-    def calculate_complex_numbers(self, values_list):
-        real_num = values_list[::2]
-        imaginary_num = values_list[1::2]
-        complex_numbers = []
-        for i, q in zip(real_num, imaginary_num):
-            complex_numbers.append(complex(i, q))
-        return complex_numbers
-
-    def calculate_magnitudes(self, values_list):
-        complex_numbers = self.calculate_complex_numbers(values_list)
-        magnitudes = [abs(val) for val in complex_numbers]
-        return magnitudes
-
-    def calculate_mean_magnitude(self, values_list):
-        magnitudes = self.calculate_magnitudes(values_list)
-        return numpy.mean(magnitudes)
-
-    def calculate_phases(self, values_list):
-        complex_numbers = self.calculate_complex_numbers(values_list)
-        phases = [math.degrees(cmath.phase(val)) for val in complex_numbers]
-        return phases
-
-    def calculate_mean_phase(self, values_list):
-        phases = self.calculate_phases(values_list)
-        return numpy.mean(phases)
-
     def cook_up_equi_indicator(self):
         equilibrium_indicator = 0
 
@@ -350,11 +379,11 @@ class Measurements:
         if self.humidity_stable:
             equilibrium_indicator = equilibrium_indicator | HUMIDITY_STABLE
 
-        if self.magnitude_stable:
-            equilibrium_indicator = equilibrium_indicator | MAGNITUDE_STABLE
+        if self.dut_signals_stable[0]:
+            equilibrium_indicator = equilibrium_indicator | DUT_SIGNAL0_STABLE
 
-        if self.phase_stable:
-            equilibrium_indicator = equilibrium_indicator | PHASE_STABLE
+        if self.dut_signals_stable[1]:
+            equilibrium_indicator = equilibrium_indicator | DUT_SIGNAL1_STABLE
 
         return equilibrium_indicator
 
@@ -366,18 +395,50 @@ class Measurements:
         return (target_hum-self.max_delta_hum <= float(readback_hum)) and \
                (float(readback_hum) <= target_hum+self.max_delta_hum)
 
-def plot_output(output_basename, measurements_appendices, show_blocking_plot, title = ''):
+    #convecinece function to close everything in case an exception escapes
+    #FIXME: needs to be called by code using the Measurements object
+    def close(self):
+        if self.chamber is not None:
+            self.chamber.close()
+        if self.instr_chamber is not None:
+            self.inst_chamber.close()
+        if self.dut is not None:
+            self.dut.close()
+        if self.ext_sensors is not None:
+            self.ext_sensors.close()
+        
+    
+def plot_output(output_basename, measurements_appendices, show_blocking_plot, config_data, ext_sens_data,
+                measurement_set_names, reference_signal_names, title=''):
+    
+    time_unit = str(config_data['time_unit'])
+
     list_of_frames = []
-    for m in measurements_appendices:
+    storepath = os.path.join(os.getcwd(), 'PostPlots')
+
+    post_plot = PostPlot.PostPlot(reference_signal_names, trace_subplot5=config_data['trace_subplot5'])
+    for index, m in enumerate(measurements_appendices):
         measurement_name = output_basename+'_'+str(m)
         list_of_frames.append(pd.read_csv(measurement_name+'.csv'))
 
+        for set_name in measurement_set_names:
+            post_plot.plot_frame_data(list_of_frames[-1], title+set_name, time_unit, set_name)
+            post_plot.save_fig(storepath, measurement_name+set_name+'.pdf')
+
     combined_data_frame = pd.concat(list_of_frames, ignore_index=True, sort=False)
+    
+    for set_name in measurement_set_names:
+        post_plot.plot_frame_data(combined_data_frame, title+set_name, time_unit, set_name)
+        post_plot.save_fig(storepath, output_basename+set_name + '.pdf')
+    
     if show_blocking_plot:
         plt.ioff()
-    plot = MeasurementPlot.MeasurementPlot(title)
+    
+    plot = MeasurementPlot.MeasurementPlot(reference_signal_names, title = title, trace_subplot5 = \
+                                           config_data['trace_subplot5'] )
     plot.draw_in_this_thread(combined_data_frame, output_basename + '_graph.pdf')
 
+
 def run_temperature_sweep_from_file(temperature_sweep_file, meas):
     with open(temperature_sweep_file) as file:
         try:
@@ -398,12 +459,6 @@ def run_temperature_sweep_from_file(temperature_sweep_file, meas):
 
 if __name__ == '__main__':
     parser = ArgumentParser()
-    parser.add_argument("-c", "--chamber",
-                        help="IP address of climate chamber", metavar="ADDR",
-                        required=True)
-    parser.add_argument("-v", "--vna",
-                        help="IP address of VNA", metavar="ADDR",
-                        required=True)
     parser.add_argument('-f', '--file',
                         help='File containing custom list of measurements',
                         default='')
@@ -428,34 +483,46 @@ if __name__ == '__main__':
         print('Argument error: Either \'file\' or \'temperaturesweepfile\' must be specified.')
         sys.exit(-1)
 
+    time_string = time.strftime("%Y_%m_%d-%H_%M_%S")
     if not args.output:
-        output_basename = time.strftime("%Y_%m_%d-%H_%M_%S") + "_results"
+        output_basename = time_string + "_results"
     else:
         output_basename = args.output
 
-    print(args.chamber, args.vna, args.file, output_basename, args.standby)
+    print(args.file, output_basename, args.standby)
 
     # reading json file for target accuracy
     with open('test_stand_parameter.json', 'r') as f:
         config_data = json.load(f)
 
-    mes = Measurements(args.chamber, args.vna, output_basename, args.standby, config_data)
+    with open('ext_sensor_channels.json', 'r') as f2:
+        ext_sensor_channels = json.load(f2)
+
+    mes = None
     try:
+        mes = Measurements(config_data, output_basename, args.standby, ext_sensor_channels)
         if args.file:
             n_measurements = mes.perform_measurements(args.file)
-            plot_output(output_basename, range(n_measurements), args.plot, output_basename)
+            plot_output(output_basename, range(n_measurements), args.plot, config_data, ext_sensor_channels,
+                        mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(), output_basename)
         if args.temperaturesweepfile:
             temperatures, humidity = run_temperature_sweep_from_file(args.temperaturesweepfile, mes)
-            #run analysis here
+            # run analysis here
             temp_extensions = []
             for t in temperatures:
                 temp_extensions.append(str(t)+'deg_'+str(humidity)+'rh')
-            analysis.plot_sweep(temperatures, [humidity]*len(temperatures), output_basename, 'temperature')
-            plot_output(output_basename, temp_extensions, args.plot, output_basename + ': Temperature sweep ' +
+            analysis.plot_sweep(temperatures, [humidity]*len(temperatures), output_basename, 'temperature',
+                                mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(),
+                                analysis_config={'type': 'default', 'normalise': [False, False],
+                                                 'extra_signal_names': [],
+                                                 'dut_name': 'measurement from prototype script',
+                                                 'time_string': time_string})
+            plot_output(output_basename, temp_extensions, args.plot, config_data, ext_sensor_channels,
+                        mes.dut.get_measurement_set_names(), mes.dut.get_dut_reference_signal_names(),
+                        output_basename + ': Temperature sweep ' +
                         str(temperatures[0]) + ' degC to ' + str(temperatures[-1]) + ' degC')
-            print(str(temp_extensions))
     finally:
-        mes.chamber.close()
+        if mes is not None:
+            mes.close()
 
     # mes.plot_output(output)
-
diff --git a/Python_script/rerun_analysis.py b/Python_script/rerun_analysis.py
new file mode 100755
index 0000000000000000000000000000000000000000..622383621866435b5eaac2070b5106ac065753b8
--- /dev/null
+++ b/Python_script/rerun_analysis.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python3
+import os
+
+import analysis
+import numpy as np
+import datetime
+
+basename = '2023_10_26-17_55_50_results'
+temperatures = np.arange(10., 32.+1., 2.)
+humidities = [55.]*len(temperatures)
+
+cable_length = 10
+dut_name = "my test cable"
+
+time_string = 're-analysis at ' + str(datetime.datetime.now())
+sweep_type = 'temperature'
+measurement_sets = ['1.3GHz', '1.0GHz', '3.0GHz', '6.0GHz', '10.0GHz']
+reference_signal_names = ['S21_PHASE', 'S21_MAGNITUDE']
+
+data_folder = os.path.join('measurements', basename)
+os.chdir(data_folder)
+
+analysis.plot_sweep(temperatures, humidities, basename, sweep_type, measurement_sets, reference_signal_names,
+                    analysis_config={'type': 'rf_cable', 'normalise': [True, False], 'cable_length': cable_length,
+                                     'extra_signal_names': ['RF_FREQUENCY'], 'dut_name': dut_name,
+                                     'time_string': time_string})
diff --git a/Python_script/test_stand_parameter.json b/Python_script/test_stand_parameter.json
index 0ba7d7240650e0f48766a60c67034aad40f822f4..ba4b2b8e58abb56b8046aab8412b7ea3a05c443d 100644
--- a/Python_script/test_stand_parameter.json
+++ b/Python_script/test_stand_parameter.json
@@ -1 +1,21 @@
-{"delta_temp": 0.1, "delta_hum": 1, "delta_mag": 0.1 , "delta_phase": 0.02, "sleep_time": 10.0, "frequency": 1300000000, "vna_config_file": "climate-lab.znxml","chamber_ip":"localhost", "vna_ip":"localhost", "data_folder":"measurements"}
+{
+  "delta_temp": 0.1,
+  "delta_hum": 1,
+  "dut": {
+    "type": "VNA",
+    "delta_mag": 0.13,
+    "delta_phase": 1.5,
+    "reference_signals" : ["S21_PHASE", "S21_MAGNITUDE"],
+    "frequencies": [1300000000, 1000000000, 3000000000, 6000000000, 10000000000],
+    "vna_ip": "mskzna43-lab",
+    "vna_config_file": "CALSetup310M.znxml"
+  },
+  "sleep_time": 10,
+  "chamber_ip": "mskclimate3",
+  "instr_chamber_ip": "mskclimate2",
+  "data_folder": "measurements",
+  "logger_ip": "mskdataloggerts",
+  "time_unit": "min",
+  "trace_subplot5": "logger_sens",
+  "logger_model": "710"
+}
diff --git a/Python_script/test_stand_parameters_deviceaccess.json b/Python_script/test_stand_parameters_deviceaccess.json
new file mode 100644
index 0000000000000000000000000000000000000000..1e43ea3154eafbb0c851099a37ee83d8dc3f5a27
--- /dev/null
+++ b/Python_script/test_stand_parameters_deviceaccess.json
@@ -0,0 +1,25 @@
+{
+  "delta_temp": 0.1,
+  "delta_hum": 1,
+  "dut": {
+    "type": "DeviceAccess",
+    "deltas": [0.13,  1.5],
+    "reference_signals" : ["encoder_position","target_position"],
+    "FIXME_dmap_file" : "FIXME has to be absolute path at the moment FIXME",
+    "dmap_file" : "/home/killenb/climate-lab-test-stand/Python_script/devices.dmap",
+    "device_alias": "delay_stage",
+    "signals" : {
+      "encoder_position" : "RB_ENCODER",
+      "target_position" : "POSITION_SP",
+      "speed": "SPEED"
+    }
+  },
+  "sleep_time": 10,
+  "chamber_ip": "localhost",
+  "instr_chamber_ip": "localhost",
+  "data_folder": "measurements",
+  "logger_ip": "localhost",
+  "time_unit": "min",
+  "trace_subplot5": "logger_sens",
+  "logger_model": "710"
+}
diff --git a/Python_script/vna_measurement.py b/Python_script/vna_measurement.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a80d112c9d5d324295413b852c6f5117d05e7eb
--- /dev/null
+++ b/Python_script/vna_measurement.py
@@ -0,0 +1,107 @@
+import VNA
+import pyvisa
+import time
+import numpy
+import math
+import cmath
+
+import dut_measurement
+
+
+class VnaMeasurement(dut_measurement.DutMeasurement):
+    def __init__(self, config_data, target_accuracy):
+        self.delta_mag = config_data['delta_mag']
+        self.delta_phase = config_data['delta_phase']
+        self.reference_signal_names = config_data['reference_signals']
+
+        self.vna = VNA.create_vna(config_data['vna_ip'], target_accuracy)
+
+        self.vna.load_config(config_data['vna_config_file'])
+        self.vna.create_new_trace("Trace1", "S11")
+        self.vna.create_new_trace("Trace2", "S12")
+        self.vna.create_new_trace("Trace3", "S21")
+        self.vna.create_new_trace("Trace4", "S22")
+
+        self.frequencies = {}
+        for f in config_data['frequencies']:
+            self.frequencies[str(f / 1e9) + 'GHz'] = f
+
+    def _get_trace_data(self, trace):
+        return self.vna.get_list_of_measurement_values(trace, "SDAT")
+
+    def get_dut_measurements(self, set_name):
+        # FIXME: The try/catch should be way down in the VNA class
+        for iteration in range(10):
+            try:
+                power = self.vna.get_current_power()
+                frequency = self.frequencies[set_name]
+                self.vna.set_cw_frequency(frequency)
+                self.vna.do_single_sweep()
+                s11 = self._get_trace_data("Trace1")
+                s12 = self._get_trace_data("Trace2")
+                s21 = self._get_trace_data("Trace3")
+                s22 = self._get_trace_data("Trace4")
+
+                return {'RF_POWER': power, 'RF_FREQUENCY': frequency,
+                        'S11_MAGNITUDE': self.calculate_mean_magnitude_db(s11),
+                        'S11_PHASE': self.calculate_mean_phase(s11),
+                        'S12_MAGNITUDE': self.calculate_mean_magnitude_db(s12),
+                        'S12_PHASE': self.calculate_mean_phase(s12),
+                        'S21_MAGNITUDE': self.calculate_mean_magnitude_db(s21),
+                        'S21_PHASE': self.calculate_mean_phase(s21),
+                        'S22_MAGNITUDE': self.calculate_mean_magnitude_db(s22),
+                        'S22_PHASE': self.calculate_mean_phase(s21),
+                        'SET_NAME': set_name}
+
+            # exception for pyvisa error or timeout
+            except pyvisa.errors.VisaIOError:
+                # call reset function for VNA status register and error queue
+                self.vna.reset_status()
+                print('An error occurred during VNA read out')
+                # wait one second for next try
+                time.sleep(1)
+        # FIXME: In case we did not succeed we need something like a stop_measurement exception
+
+        raise Exception('FIXME: Throw stop_measurement here, and handle it. Don\'t crash!')
+
+    def get_dut_signal_names(self):
+        return ['RF_POWER', 'RF_FREQUENCY', 'S11_MAGNITUDE', 'S11_PHASE', 'S12_MAGNITUDE',
+                'S12_PHASE', 'S21_MAGNITUDE', 'S21_PHASE', 'S22_MAGNITUDE', 'S22_PHASE', 'SET_NAME']
+
+    def get_dut_reference_signal_names(self):
+        return self.reference_signal_names
+
+    def get_dut_max_delta_signals(self):
+        return [self.delta_mag, self.delta_phase]
+
+    def calculate_complex_numbers(self, values_list):
+        real_num = values_list[::2]
+        imaginary_num = values_list[1::2]
+        complex_numbers = []
+        for i, q in zip(real_num, imaginary_num):
+            complex_numbers.append(complex(i, q))
+        return complex_numbers
+
+    def calculate_magnitudes(self, values_list):
+        complex_numbers = self.calculate_complex_numbers(values_list)
+        magnitudes = [abs(val) for val in complex_numbers]
+        return magnitudes
+
+    def calculate_mean_magnitude(self, values_list):
+        magnitudes = self.calculate_magnitudes(values_list)
+        return numpy.mean(magnitudes)
+
+    def calculate_mean_magnitude_db(self, values_list):
+        return 20*math.log10(self.calculate_mean_magnitude(values_list))
+
+    def calculate_phases(self, values_list):
+        complex_numbers = self.calculate_complex_numbers(values_list)
+        phases = [math.degrees(cmath.phase(val)) for val in complex_numbers]
+        return phases
+
+    def calculate_mean_phase(self, values_list):
+        phases = self.calculate_phases(values_list)
+        return numpy.mean(phases)
+
+    def get_measurement_set_names(self):
+        return list(self.frequencies.keys())