from PyQt6.QtGui import * from PyQt6.QtWidgets import * from PyQt6.QtCore import * import multiprocessing import multiprocessing.managers import time from datetime import datetime import traceback, sys, os, subprocess import numpy as np import pyqtgraph as pg # Get the current script's directory current_dir = os.path.dirname(os.path.abspath(__file__)) # Get the parent directory by going one level up parent_dir = os.path.dirname(current_dir) # Add the parent directory to sys.path sys.path.append(parent_dir) from drivers import Keysight_34461A import import_txt from design_files.Keysight_34461A_design import Ui_MainWindow class WorkerSignals(QObject): ''' Defines the signals available from a running worker thread. Supported signals are: finished: No data error: tuple (exctype, value, traceback.format_exc() ) result: object data returned from processing, anything progress: int indicating % progress ''' finished = pyqtSignal() error = pyqtSignal(tuple) result = pyqtSignal(object) progress = pyqtSignal(list) class Worker(QRunnable): ''' Worker thread Inherits from QRunnable to handler worker thread setup, signals and wrap-up. :param callback: The function callback to run on this worker thread. Supplied args and kwargs will be passed through to the runner. :type callback: function :param args: Arguments to pass to the callback function :param kwargs: Keywords to pass to the callback function ''' def __init__(self, fn, *args, **kwargs): super(Worker, self).__init__() # Store constructor arguments (re-used for processing) self.fn = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() # Add the callback to our kwargs self.kwargs['progress_callback'] = self.signals.progress @pyqtSlot() def run(self): ''' Initialise the runner function with passed args, kwargs. ''' # Retrieve args/kwargs here; and fire processing using them try: result = self.fn(*self.args, **self.kwargs) except: traceback.print_exc() exctype, value = sys.exc_info()[:2] self.signals.error.emit((exctype, value, traceback.format_exc())) else: self.signals.result.emit(result) # Return the result of the processing finally: self.signals.finished.emit() # Done def get_float(Qline,default = 0): #gets value from QLineEdit and converts it to float. If text is empty or cannot be converted, it returns "default" which is 0, if not specified try: out = float(Qline.text()) except: out = default return(out) class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, *args, **kwargs): # Get the current script's directory self.current_dir = os.path.dirname(os.path.abspath(__file__)) # Get the parent directory by going one level up self.parent_dir = os.path.dirname(current_dir) #establish connection to global variables try: #try to connect to global variables manager = multiprocessing.managers.BaseManager(address=('localhost',5001), authkey=b'') manager.connect() manager.register('sync_K_34461A') self.sync_K_34461A = manager.sync_K_34461A() except: #open global variables, if no connection can be made (i.e. it is not running). Then connect to it # subprocess.call(['D:\\Python instrument drivers\\env\\Scripts\\python.exe', 'D:\\Python instrument drivers\\StandAlones\\global_variables.py']) self.global_vars = QProcess() self.global_vars.start(self.current_dir+"\\env\\Scripts\\python.exe", [self.current_dir+'\\global_variables.py']) manager.connect() manager.register('sync_K_34461A') self.sync_K_34461A = manager.sync_K_34461A() print('!!!\nI opened global variables myself. If you close me, global variables will shut down too. Consider starting global variables in own instance for more security\n!!!') #Set default values in global variables. V (Value) is the measurement data, mode specifies current or voltage measurement (0 = voltage, 1 = current) self.sync_K_34461A.update({'V':0, 'mode':0, 'sensor':['TC','R']}) #import Gui from QT designer file super(MainWindow, self).__init__(*args, **kwargs) self.setupUi(self) #setup plot self.graphWidget.setBackground('w') self.graphWidget.setLabel('left', 'Voltage [V]') self.graphWidget.setLabel('bottom', 'Time (H)') axis = pg.DateAxisItem() self.graphWidget.setAxisItems({'bottom':axis}) temp = [time.time(),time.time()-1] pen1 = pg.mkPen(color=(255, 0, 0), width=2) self.plot_P = self.graphWidget.plot(temp,[1,0],pen = pen1, name = 'Voltage') #set up pyQT threadpool self.threadpool = QThreadPool() #start standard threads worker_save = Worker(self.save) self.threadpool.start(worker_save) #define signals and slots self.actionSet_default.triggered.connect(self.set_default) self.actionReset_default.triggered.connect(self.read_default) self.button_connect.clicked.connect(self.start_meas) self.line_Nplot.editingFinished.connect(self.set_Npoints) self.line_saveInterval.editingFinished.connect(self.change_timing) self.checkBox_disableplots.stateChanged.connect(self.set_displot) self.comboBox_Plot.currentIndexChanged.connect(self.change_plot) self.comboBox_Plot.currentIndexChanged.connect(self.change_mode) self.comboBox_sensor.currentIndexChanged.connect(self.change_sensor) #define constants self.measure = 0 #which measurement should be taken. 0 = voltage, 1 = current self.Values = np.zeros((1)) #store Voltage or Current data self.t = [time.time()] #store timestamps self.t1 = [datetime.now()] #store timestamps with higher precision self.last_save = self.t1[-1] #timestamp of last write-to-file event self.Npoints = 200 #number of point to plot self.running = True #true while app is running self.disable_plot = False #constant to disable plot to improve performance. Is changed by checkbox checkBox_disableplots self.timing_save = 1 #save intervall self.lines_config_float = [self.line_Nplot,self.line_saveInterval] #is used for config file self.lines_config_strings = [self.line_devAdr,self.line_filePath] #is used for config file self.checkboxes_config = [self.checkBox_disableplots,self.checkBox_save] #is used for config file #read default values from config and set them in gui self.read_default() #write current gui values to global vars self.change_mode() #update save intervall self.change_timing() def start_meas(self): #Connect to device and configure it to measure what is selected in comboBox_Plot. Also sets the correct mode in global variables and chooses correct plot layout address = self.line_devAdr.text() self.MM = Keysight_34461A.Keysight34461A(address) if self.comboBox_Plot.currentIndex() == 0: self.MM.configure() #measure Voltage self.sync_K_34461A.update({'mode':0}) #sets mode in global variables to the one set in gui self.change_plot(0) #setup plot to show voltage elif self.comboBox_Plot.currentIndex() == 1: self.MM.configure(VoC = 'CURR', DC = 'DC') #measure current self.sync_K_34461A.update({'mode':1}) #sets mode in global variables to the one set in gui self.change_plot(1) #setup plot to show current elif self.comboBox_Plot.currentIndex() == 2: self.MM.configure_temp(Sensor='TC',Type='R') self.sync_K_34461A.update({'mode':2}) #sets mode in global variables to the one set in gui self.change_plot(1) #setup plot to show current self.MM.initiate_measurement() #start measurement #start thread for communication with device self.worker = Worker(self.update_Value) self.worker.signals.progress.connect(self.update_gui) self.threadpool.start(self.worker) def update_Value(self, progress_callback): #gets new measurement. If "mode" in global variables changed from last iteration the new measurement mode is set. self.mode_old = self.sync_K_34461A.get('mode') #is needed to compare measurement mode in global variables from preveous iteration to the one in current iteration self.sensor_old = self.sync_K_34461A.get('mode') #is needed to compare sensor type in global variables from preveous iteration to the one in current iteration while self.running == True: self.mode_new = self.sync_K_34461A.get('mode') #up to date measurement mode in global variabels self.sensor_new = self.sync_K_34461A.get('sensor') if self.mode_new != self.mode_old or self.sensor_new != self.sensor_old: #if measurement mode or sensor type was changed in gui, the device is re-configured print(self.mode_new) print(self.sensor_new) self.comboBox_Plot.setCurrentIndex(self.mode_new) self.MM.stop_measurement() #stop measurement so measurement mode can be changed if self.mode_new == 0: self.MM.configure() #measure Voltage self.MM.initiate_measurement() #restart measurement elif self.mode_new == 1: self.MM.configure(VoC = 'CURR', DC = 'DC') #measure current self.MM.initiate_measurement() #restart measurement elif self.mode_new == 2: self.MM.configure_temp(Sensor=self.sensor_new[0],Type=self.sensor_new[1]) #measure tmperaure self.MM.initiate_measurement() #restart measurement Val = self.MM.read() self.sync_K_34461A.update({'V':Val}) progress_callback.emit([Val]) self.mode_old = self.mode_new #store measurement mode from this iteration in mode_old so it can be compared to the new global variables in the next iteration self.sensor_old = self.sensor_new #strore sensor type from this iteration in sensor_old so it can be compared to the new global variables in the next iteration time.sleep(0.5) del(self.MM) #disconnect device when self.running is set to False def update_gui(self,V): #set numbers depending on what is plotted if self.mode_new == 0: self.line_V.setText(f"{V[0]:.3e}") elif self.mode_new == 1: self.line_A.setText(f"{V[0]:.3e}") elif self.mode_new == 2: self.line_Temp.setText(f"{V[0]:.3e}") #Create database for plotting self.Values = np.vstack([self.Values, np.array(V[0])]) self.t.append(time.time()) self.t1.append(datetime.now()) #plot if self.disable_plot == False: self.plot_P.setData(self.t[-self.Npoints:],self.Values[-self.Npoints:,0]) def change_plot(self,I): #changes plot axis labels, and resets stored data for plotting if I == 0: self.graphWidget.setLabel('left', 'Voltage [V]') self.Values = np.zeros((1)) #reset Voltage or Current data self.t = [time.time()] #reset timestamps self.line_Temp.setText('NaN') #set text in 'temperature' field to NaN, to indicate it is not measured self.line_A.setText('NaN') # set text in 'current' field to NaN, to indicate it is not measured elif I == 1: self.graphWidget.setLabel('left', 'Current [A]') self.Values = np.zeros((1)) #reset Voltage or Current data self.t = [time.time()] #reset timestamps self.line_Temp.setText('NaN') #set text in 'temperature' field to NaN, to indicate it is not measured self.line_V.setText('NaN') # set text in 'voltage' field to NaN, to indicate it is not measured elif I == 2: self.graphWidget.setLabel('left', 'Temperaure [C°]') self.Values = np.zeros((1)) #reset Voltage or Current data self.t = [time.time()] #reset timestamps self.line_V.setText('NaN') # set text in 'voltage' field to NaN, to indicate it is not measured self.line_A.setText('NaN') # set text in 'Current' field to NaN, to indicate it is not measured def change_mode(self): #updates "mode" in global variables. This is detected by "update_Value" and the device is set to the correct mode self.sync_K_34461A.update({'mode':self.comboBox_Plot.currentIndex()}) def change_sensor(self,I): #updates "sensor" in global variables. "update_Value" and the device is set to the correct mode if I == 0: #assign the correct sensor types to selected Index (Manual p.251) self.sync_K_34461A.update({'sensor':['RTD','85']}) elif I == 1: self.sync_K_34461A.update({'sensor':['FRTD','82']}) elif I == 2: self.sync_K_34461A.update({'sensor':['FTH','5000']}) elif I == 3: self.sync_K_34461A.update({'sensor':['THER','5000']}) elif I == 4: self.sync_K_34461A.update({'sensor':['TC','E']}) elif I == 5: self.sync_K_34461A.update({'sensor':['TC','J']}) elif I == 6: self.sync_K_34461A.update({'sensor':['TC','K']}) elif I == 7: self.sync_K_34461A.update({'sensor':['TC','N']}) elif I == 8: self.sync_K_34461A.update({'sensor':['TC','R']}) elif I == 9: self.sync_K_34461A.update({'sensor':['TC','T']}) def change_timing(self): #updates the timing which is used for "save". If no value it given, it is set to 1 s self.timing_save = get_float(self.line_saveInterval,1) def save(self, progress_callback): #if save checkbox is checked it writes measurement values to file specified in line.filePath. There the full path including file extension must be given. while self.running == True: time.sleep(self.timing_save) #wait is at beginning so first point is not corrupted when app just started. if self.checkBox_save.isChecked() == True and self.t1[-1] > self.last_save: #write only, if there is a new timestamp path = self.line_filePath.text() if os.path.isfile(path) == False: with open(path,'a') as file: file.write('date\tVoltage[V]\tCurrent[A]\tTemperature[°C]\n') file = open(path,'a') #file.write(time.strftime("%Y-%m-%d_%H-%M-%S",time.localtime(self.t[-1]))+'\t') file.write(self.t1[-1].strftime("%Y-%m-%d_%H-%M-%S.%f")+'\t') if self.mode_new == 0: file.write(f"{self.sync_K_34461A.get('V')}\t0\t0\n") elif self.mode_new == 1: file.write(f"0\t{self.sync_K_34461A.get('V')}\t0\n") elif self.mode_new == 2: file.write(f"0\t0\t{self.sync_K_34461A.get('V')}\n") self.last_save = self.t1[-1] file.close def set_Npoints(self): #sets the number of points to plot self.Npoints = int(self.line_Nplot.text()) def set_displot(self): #sets variable to disable plot so checkbox state does not need be read out every iteration self.disable_plot = self.checkBox_disableplots.isChecked() def set_default(self): #saves current set values to txt file in subdirectory configs. All entries that are saved are defined in self.lines_config #Additionally measurement mode is saved. Overwrites old values in config file. path = self.current_dir+'\\configs\\Keysight_34461A_config.txt' #To make shure the config file is at the right place, independent from where the program is started the location of the file is retrieved file = open(path,'w') for l in self.lines_config_float: temp = f"{get_float(l)}" file.write(temp+'\t') for l in self.lines_config_strings: file.write(l.text()+'\t') for c in self.checkboxes_config: file.write(str(c.isChecked())+'\t') file.write(str(self.comboBox_Plot.currentIndex())+'\t') file.write(str(self.comboBox_sensor.currentIndex())) file.write('\n') file.close def read_default(self): #reads default values from config file in subdirectory config and sets the values in gui. Then self.change is set to true so values are send #to device. (If no config file exists, it does nothing.) path = self.current_dir+'\\configs\\Keysight_34461A_config.txt' #To make shure the config file is read from the right place, independent from where the program is started the location of the file is retrieved try: #exit function if config file does not exist vals = import_txt.read_raw(path) except: return formats = ['.0f','.2f'] for l,v,f in zip(self.lines_config_float,vals[0],formats): v = float(v) #convert string in txt to float, so number can be formatted according to "formats" when it's set l.setText(format(v,f)) for l,v in zip(self.lines_config_strings,vals[0][len(self.lines_config_float):]): l.setText(v) for c,v in zip(self.checkboxes_config,vals[0][len(self.lines_config_float)+len(self.lines_config_strings):]): c.setChecked(v == 'True') self.comboBox_Plot.setCurrentIndex(int(vals[0][-2])) self.comboBox_sensor.setCurrentIndex(int(vals[0][-1])) self.change = True def closeEvent(self,event): #when window is closed self.running is set to False, so all threads stop. self.running = False time.sleep(1) #make shure threads have enough time to close event.accept() app = QApplication(sys.argv) window = MainWindow() window.show() app.exec()