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

375 lines
19 KiB
Python

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