craft-software/Legacy/Instrument_Drivers/Agilent_304_control.py
2025-07-04 15:52:40 +02:00

347 lines
15 KiB
Python

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()