from PyQt6.QtGui import * from PyQt6.QtWidgets import * from PyQt6.QtCore import * import multiprocessing import multiprocessing.managers import time import traceback import sys import os from timeit import default_timer as timer import collections import itertools import numpy as np import pyqtgraph as pg import import_txt from random import random # 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 Agilent_304_FS_AG from design_files.Agilent_304_FS_AG_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_AG_304') self.sync_AG_304 = manager.sync_AG_304() 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_AG_304') self.sync_AG_304 = manager.sync_AG_304() 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!!!') #fill in variables, if they are not defined in global variables self.sync_AG_304.update({'P':0, 'T':0, 'f':0, 'Status': 'Stop'}) #import Gui from QT designer file super(MainWindow, self).__init__(*args, **kwargs) self.setupUi(self) #setup plot self.graphWidget.setBackground('w') self.graphWidget.setTitle("Pressure") self.graphWidget.setLabel('left', 'Pressure [mnbar]') self.graphWidget.setLabel('bottom', 'Time (H)') axis = pg.DateAxisItem() self.graphWidget.setAxisItems({'bottom':axis}) self.graphWidget.setLogMode(False,True) temp = [time.time(),time.time()-1] pen1 = pg.mkPen(color=(255, 0, 0), width=2) self.plot_1 = self.graphWidget.plot(temp,[1,0],pen = pen1) #set up pyQT threadpool self.threadpool = QThreadPool() #define and 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.SB_N_points.valueChanged.connect(self.set_Npoints) self.dSB_save_intervall.valueChanged.connect(self.change_timing) self.checkBox_disableplots.stateChanged.connect(self.set_displot) self.button_buffer_size.clicked.connect(self.change_buffer_size) self.Button_start.clicked.connect(self.start_pump) self.Button_stop.clicked.connect(self.stop_pump) #define constants self.P = 0 #store Pressure data self.start = False #start Pump? Is set to true be self.start_pump self.stop = False #Stop Pump? Is set to true be self.stop_pump 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 = 5 #save intervall [s] self.buffer_size = 36000 #size of ringbuffers for plotting. 36000 is roughly 1 hour, if timng_main is 0.1 s self.buffer = collections.deque(maxlen=self.buffer_size) #ringbuffer for plot data self.lines_config_strings = [self.line_devAdr,self.line_filePath]#is used for config file self.Spinboxes_config = [self.SB_N_points, self.dSB_save_intervall, self.dSB_timing, self.SB_Buffer_size]#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() #update save intervall to the gui value self.change_timing() def start_meas(self): #Connect to device address = self.line_devAdr.text() self.ag = Agilent_304_FS_AG.AG_304_FS(address) #start thread for communication with device self.worker = Worker(self.update_all) self.worker.signals.progress.connect(self.update_gui) self.threadpool.start(self.worker) def update_all(self, progress_callback): '''get values from device and write them to global variables.''' while self.running == True: start = timer() P = self.ag.read_pressure() T = self.ag.read_temp() f = self.ag.read_freq() status = self.ag.read_status() self.P = P #store pressure as global variable for save thread #Write measurement values into global variables self.sync_AG_304.update({'P':P}) self.sync_AG_304.update({'T':T}) self.sync_AG_304.update({'f':f}) self.sync_AG_304.update({'Status':status}) #Append Ringbuffer self.buffer.append([time.time(),P]) #emit signal to trigger self.update_gui progress_callback.emit([P,T,f,status]) end = timer() if self.start == True: #check if pump should start or stop self.ag.start_pump() time.sleep(2) self.start = False elif self.stop == True: self.ag.stop_pump() time.sleep(2) self.stop = False try: time.sleep(self.dSB_timing.value()-(end-start)) except: print(f"Iteration took {end-start} seconds.") del(self.ag) #disconnect device when self.running is set to False def update_gui(self, List): #Convert List into original format P = List[0] T = List[1] f = List[2] status = List[3] #set numbers self.line_P.setText(f"{P:.2e}") self.line_T.setText(f"{T:.2f}") self.line_f.setText(f"{f}") self.line_status.setText(status) #plot #calculate index in buffer where plots start. (Needed for itertools.islice) I_s = len(self.buffer) - self.Npoints if I_s < 0: I_s = 0 if self.disable_plot == False: self.plot_1.setData(np.array(list(itertools.islice(self.buffer,I_s,None)))[:,0], np.array(list(itertools.islice(self.buffer,I_s,None)))[:,1]) def start_pump(self): '''generates prompt to make shure pump should start and, depending on the input, it sets self.start to True. It is done this way so the controller does not get multiple inputs at once from different threads.''' button = QMessageBox.question(self, 'Start Pump','Start Pump?') if button == QMessageBox.StandardButton.Yes: print('start') self.start = True else: print('no start') def stop_pump(self): '''generates prompt to make shure pump should stop and, depending on the input, it sets self.stop to True. It is done this way so the controller does not get multiple inputs at once from different threads.''' button = QMessageBox.question(self, 'Stop Pump','Stop Pump?') if button == QMessageBox.StandardButton.Yes: print('stop') self.stop = True else: print('no stop') def change_buffer_size(self): new_len = self.SB_Buffer_size.value() data = list(self.buffer) self.buffer = collections.deque(data[-new_len:], maxlen = new_len) def set_Npoints(self): #sets the number of points to plot self.Npoints = self.SB_N_points.value() 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 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 = self.dSB_save_intervall.value() 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: path = self.line_filePath.text() if os.path.isfile(path) == False: with open(path,'a') as file: file.write('date\tP[mbar]\n') file = open(path,'a') t = self.buffer[len(self.buffer)-1][0] file.write(time.strftime("%Y-%m-%d_%H-%M-%S",time.localtime(t))+'\t') file.write(f'{self.P}\n') file.close 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 #Overwrites old values in config file. current_dir = os.path.dirname(os.path.abspath(__file__)) path = current_dir+'\\configs\\AG_304_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_strings: file.write(l.text()+'\t') for s in self.Spinboxes_config: file.write(f"{s.value()}\t") for c in self.checkboxes_config: file.write(str(c.isChecked())+'\t') 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.) current_dir = os.path.dirname(os.path.abspath(__file__)) path = current_dir+'\\configs\\AG_304_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: print('no config file found on') print(path) return for l,v in zip(self.lines_config_strings,vals[0][:]): l.setText(v) for s,v in zip(self.Spinboxes_config,vals[0][len(self.lines_config_strings):]): try: #give float or int, depending on dSB of SB s.setValue(float(v)) except TypeError: s.setValue(int(v)) for c,v in zip(self.checkboxes_config,vals[0][len(self.lines_config_strings):]): c.setChecked(v == 'True') 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) event.accept() app = QApplication(sys.argv) window = MainWindow() window.show() app.exec()