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

434 lines
21 KiB
Python

from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
import multiprocessing
import multiprocessing.managers
import time
import traceback,sys,os
import numpy as np
import pyqtgraph as pg
import import_txt
import collections
import itertools
# 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 BK_9132B
from design_files.BK_9131B_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_BK_9131B')
self.sync_BK_9131B = manager.sync_BK_9131B()
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_BK_9131B')
self.sync_BK_9131B = manager.sync_BK_9131B()
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_BK_9131B.update({'setU':[0,0,0], 'setI':[0,0,0], 'OutputOn':[False, False, False], 'U':[0,0,0], 'I':[0,0,0], 'P':[0,0,0]})
#import Gui from QT designer file
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
#setup plot
self.graphWidget.setBackground('w')
self.graphWidget.setTitle("Voltage, Current, Power")
self.graphWidget.setLabel('left', 'Voltage [V], Current [A], Power [W]')
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)
pen2 = pg.mkPen(color=(0, 0, 255), width=2)
pen3 = pg.mkPen(color=(0, 255, 0), width=2)
pen4 = pg.mkPen(color=(255, 255, 0), width=2)
pen5 = pg.mkPen(color=(255, 0, 255), width=2)
pen6 = pg.mkPen(color=(0, 255, 255), width=2)
pen7 = pg.mkPen(color=(255, 127, 127), width=2)
pen8 = pg.mkPen(color=(127, 255, 70), width=2)
pen9 = pg.mkPen(color=(255, 127, 70), width=2)
self.plot_1 = self.graphWidget.plot(temp,[1,0],pen = pen1, name = 'V Ch: 1')
self.plot_2 = self.graphWidget.plot(temp,[1,0],pen = pen2, name = 'I Ch: 1')
self.plot_3 = self.graphWidget.plot(temp,[1,0],pen = pen3, name = 'P Ch: 1')
self.plot_4 = self.graphWidget.plot(temp,[1,0],pen = pen4, name = 'V Ch: 2')
self.plot_5 = self.graphWidget.plot(temp,[1,0],pen = pen5, name = 'I Ch: 2')
self.plot_6 = self.graphWidget.plot(temp,[1,0],pen = pen6, name = 'P Ch: 2')
self.plot_7 = self.graphWidget.plot(temp,[1,0],pen = pen7, name = 'V Ch: 3')
self.plot_8 = self.graphWidget.plot(temp,[1,0],pen = pen8, name = 'I Ch: 3')
self.plot_9 = self.graphWidget.plot(temp,[1,0],pen = pen9, name = 'P Ch: 3')
self.graphWidget.addLegend()
#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_Nplot.editingFinished.connect(self.set_Npoints)
self.dSB_saveInterval.editingFinished.connect(self.change_timing)
self.checkBox_disableplots.stateChanged.connect(self.set_displot)
self.checkBox_V_1.stateChanged.connect(self.plot_hide)
self.checkBox_I_1.stateChanged.connect(self.plot_hide)
self.checkBox_P_1.stateChanged.connect(self.plot_hide)
self.checkBox_V_2.stateChanged.connect(self.plot_hide)
self.checkBox_I_2.stateChanged.connect(self.plot_hide)
self.checkBox_P_2.stateChanged.connect(self.plot_hide)
self.checkBox_V_3.stateChanged.connect(self.plot_hide)
self.checkBox_I_3.stateChanged.connect(self.plot_hide)
self.checkBox_P_3.stateChanged.connect(self.plot_hide)
self.V_set_1.editingFinished.connect(self.set_V)
self.I_set_1.editingFinished.connect(self.set_I)
self.Output_1.stateChanged.connect(self.set_Output)
self.V_set_2.editingFinished.connect(self.set_V)
self.I_set_2.editingFinished.connect(self.set_I)
self.Output_2.stateChanged.connect(self.set_Output)
self.V_set_3.editingFinished.connect(self.set_V)
self.I_set_3.editingFinished.connect(self.set_I)
self.Output_3.stateChanged.connect(self.set_Output)
#define constants
self.Voltage = np.zeros((1,3)) #store voltage data
self.Current = np.zeros((1,3)) #store current data
self.Power = np.zeros((1,3)) #store power data
self.t = [time.time()] #store timestamps
self.Npoints = 200 #number of point to plot
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.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.set_old = [0,0,1] #variable to save the 'old' set values to compare them to the global variables. It differs from set_new in the first iteration. This ensures that new parameters are send to the device
self.set_new = [0,0,0] #variable to save the new set values to compare them to the old ones
self.Spinboxes_config = [self.V_set_1,self.I_set_1,self.V_set_2,self.I_set_2,
self.V_set_3,self.I_set_3,self.SB_Nplot, self.SB_Buffer_size,
self.dSB_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.Output_1, self.Output_2, self.Output_3,self.checkBox_disableplots,self.checkBox_save,
self.checkBox_V_1, self.checkBox_I_1, self.checkBox_P_1, self.checkBox_V_2, self.checkBox_I_2, self.checkBox_P_2,
self.checkBox_V_3, self.checkBox_I_3, self.checkBox_P_3,]#is used for config file
#read default values from config and set them in gui
self.read_default()
# #write values from gui to global variables.
self.set_V()
self.set_I()
self.set_Output()
#update save intervall to the gui value
self.change_timing()
def start_meas(self):
#Connect to device
address = self.line_devAdr.text()
self.BK = BK_9132B.BK_9132B(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. Checks if global variables changed from last iteration. Also pass it to upddate_gui with emit(T)
while self.running == True:
for i,n in enumerate(['setU', 'setI', 'OutputOn']): #get new set values from global variables and compare to old ones.
self.set_new[i] = self.sync_BK_9131B.get(n)
if self.set_new != self.set_old: #if a button is clicked or global variables are changed the program checks which setting has been changed.
if self.set_new[0] != self.set_old[0]: #if setU is changed new setU parameters are sent to device
for CH in [1,2,3]: #for loop for all 3 channels
self.BK.set_Voltage(CH, self.set_new[0][CH-1]) #CH is element of 1-3 but setU parameters have indices of 0-2
if self.set_new[1] != self.set_old[1]: #if setI is changed new setI parameters are sent to device
for CH in [1,2,3]: #for loop for all 3 channels
self.BK.set_Current(CH, self.set_new[1][CH-1]) #CH is element of 1-3 but setI parameters have indices of 0-2
if self.set_new[2] != self.set_old[2]: #if OutputOn is changed new OutputOn parameters are sent to device
for CH in [1,2,3]: #for loop for all 3 channels
self.BK.set_Output(CH, self.set_new[2][CH-1]) #CH is element of 1-3 but OutputOn parameters have indices of 0-2
self.update_setValues(self.set_new) #Change GUI
U = self.BK.read_Voltage() #read Voltage of all three channels
I = self.BK.read_Current() #read Current of all three channels
P = self.BK.read_Power() #read Power of all three channels
#store values for save thread
self.Voltage = U
self.Current = I
self.Power = P
#Write measurement values into global variables
self.sync_BK_9131B.update({'U':U})
self.sync_BK_9131B.update({'I':I})
self.sync_BK_9131B.update({'P':P})
#Append Ringbuffer
self.buffer.append([time.time()]+U+I+P)
#emit signal to trigger self.update_gui
progress_callback.emit([U,I,P]) #Emits list of all three lists U,I,P
self.set_old = self.set_new[:] #List needs to be sliced so that only values are taken and not just a pointer is created
# time.sleep(0.1)
del(self.BK) #disconnect device when self.running is set to False
def update_gui(self, List):
#Convert List into original format
U = List[0]
I = List[1]
P = List[2]
#set numbers
self.V_1.setText(str(U[0]))
self.I_1.setText(str(I[0]))
self.P_1.setText(str(P[0]))
self.V_2.setText(str(U[1]))
self.I_2.setText(str(I[1]))
self.P_2.setText(str(P[1]))
self.V_3.setText(str(U[2]))
self.I_3.setText(str(I[2]))
self.P_3.setText(str(P[2]))
#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:
t = np.array(list(itertools.islice(self.buffer,I_s,None)))[:,0]
self.plot_1.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,1])#V1
self.plot_2.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,4])#I1
self.plot_3.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,7])#P1
self.plot_4.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,2])#V2
self.plot_5.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,5])#I2
self.plot_6.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,8])#P2
self.plot_7.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,3])#V3
self.plot_8.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,6])#I3
self.plot_9.setData(t, np.array(list(itertools.islice(self.buffer,I_s,None)))[:,9])#P3
def update_setValues(self,setV):
#sets setvalues obtained from update_all in gui ['setU', 'setI', 'OutputOn']
self.V_set_1.setValue(setV[0][0])
self.I_set_1.setValue(setV[1][0])
self.Output_1.setChecked(setV[2][0])
self.V_set_2.setValue(setV[0][1])
self.I_set_2.setValue(setV[1][1])
self.Output_2.setChecked(setV[2][1])
self.V_set_3.setValue(setV[0][2])
self.I_set_3.setValue(setV[1][2])
self.Output_3.setChecked(setV[2][2])
def set_V(self):
#updates the set voltage in global variables. The change will be detected by update_all and it will be passed to the device
setU = [self.V_set_1.value(), self.V_set_2.value(), self.V_set_3.value()]
print(setU)
self.sync_BK_9131B.update({'setU':setU})
def set_I(self):
#updates the set current in global variables. The change will be detected by update_all and it will be passed to the device
setI = [self.I_set_1.value(), self.I_set_2.value(), self.I_set_3.value()]
self.sync_BK_9131B.update({'setI':setI})
def set_Output(self):
#updates the set Output in global variables. The change will be detected by update_all and it will be passed to the device
setOutput = [self.Output_1.isChecked(), self.Output_2.isChecked(), self.Output_3.isChecked()]
self.sync_BK_9131B.update({'OutputOn':setOutput})
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_Nplot.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 plot_hide(self):
#shows or hides plots according to the checkboxes next to the plot area
boxes = [self.checkBox_V_1, self.checkBox_I_1, self.checkBox_P_1,
self.checkBox_V_2, self.checkBox_I_2, self.checkBox_P_2,
self.checkBox_V_3, self.checkBox_I_3, self.checkBox_P_3]
plots = plots = [self.plot_1, self.plot_2, self.plot_3, self.plot_4, self.plot_5,
self.plot_6, self.plot_7, self.plot_8, self.plot_9]
for b,p in zip(boxes,plots):
if b.isChecked() == True:
p.show()
else:
p.hide()
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_saveInterval.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\tV Ch:1[V]\tI Ch:1[A]\tP Ch:1[W]\tV Ch:2[V]\tI Ch:2[A]\tP Ch:2[W]\tV Ch:3[V]\tI Ch:3[A]\tP Ch:3[W]\n')
file = open(path,'a')
file.write(time.strftime("%Y-%m-%d_%H-%M-%S",time.localtime(t))+'\t')
for i in [0,1,2]: #Loop for all three channels
file.write(f"{self.Voltage[i]}\t")
file.write(f"{self.Current[i]}\t")
file.write(f"{self.Power[i]}\t")
file.write('\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\\BK9131B_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 s in self.Spinboxes_config:
file.write(f"{s.value()}\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('\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\\BK9131B_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 s,v in zip(self.Spinboxes_config,vals[0]):
try: #give float or int, depending on dSB of SB
s.setValue(float(v))
except TypeError:
s.setValue(int(v))
for l,v in zip(self.lines_config_strings,vals[0][len(self.Spinboxes_config):]):
l.setText(v)
for c,v in zip(self.checkboxes_config,vals[0][len(self.Spinboxes_config)+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()