diff --git a/.gitignore b/.gitignore index 7348e2b..4b332d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ -*.wpr *.pyc +*.dat +.vscode/ +*.wpr +/measdata_* +*.log +log.txt +20112117.xml +config*.yml diff --git a/Arduino.py b/Arduino.py deleted file mode 100644 index c10d9ad..0000000 --- a/Arduino.py +++ /dev/null @@ -1,120 +0,0 @@ -import serial -import random -import re - -global ser_py, debug_arduino, test_arduino - - -def my_split(s): - return filter(None, re.split(r'(\d+)', s)) - - -########################################################################### -def Init_Arduino(i, com, rate, bits, stop, parity, dbg, test): -########################################################################### - - global ser_py, debug_arduino, test_arduino - - debug_arduino = dbg - test_arduino = test - if i == 0: - ser_py = list(range(0)) - portName = 'COM'+str(com) - try: - serial.Serial(port=portName) - except serial.SerialException: - print ('Port ' + portName + ' not present') - if not test_arduino: - quit() - - if not test_arduino: - ser_py_ = serial.Serial( - port = portName, - baudrate = int(rate), - parity = parity, - stopbits = int(stop), - bytesize = int(bits), - timeout = 2.0) - - ser_py.append(ser_py_) - - - -########################################################################### -def my_Read_Arduino(i, end_id): -########################################################################### - - end_of_read = False - st = '' - while not end_of_read: - char = ser_py[i].read(1).decode() - st = st + char - st = st.replace('\r', '\\r') - st = st.replace('\n', '\\n') - if end_id in st : - end_of_read = True - return (st) - -########################################################################### -def Read_Arduino(i, cmd_list, read_id_list, end_id_list, position_list, separator_list): -########################################################################### - - result = list(range(0)) - for j in range(len(cmd_list)): - if not test_arduino: - if debug_arduino: - print ('Sending to ' + ser_py[i].port + ': ' + cmd_list[j]) - ser_py[i].write((cmd_list[j]+'\r').encode()) - #_temp = ser_py[i].readline().decode() - #_temp = _temp.rstrip() - _temp = my_Read_Arduino(i, end_id_list[j]) - else: - if cmd_list[j] == 'g': - _temp = 'g=41.19\\r\\n' - elif cmd_list[j] =='c': - _temp = 'c=24.50' - elif cmd_list[j] == 'h': - _temp = 'h=1' - elif cmd_list[j] =='IN_PV_1': - _temp = '0.0 1' - elif cmd_list[j] =='GN_PV_2': - _temp = '25.0 2' - elif cmd_list[j] =='HN_X': - _temp = '14.5XSTART=23.4;99.99;15;XEND77.7;' - else: - _temp = '' - - if debug_arduino: - print ('reading from COM port: ', _temp) - - #_temp = _temp.replace('\r', '\\r') - #_temp = _temp.replace('\n', '\\n') - - if read_id_list[j] in _temp: - pos = _temp.find(read_id_list[j]) - pos2 = pos + len(read_id_list[j]) - temp = _temp[pos2:] - if end_id_list[j] in temp: - pos = temp.find(end_id_list[j]) - temp = temp[:pos] - if position_list[j] != '': - temp = temp.split(separator_list[j])[int(position_list[j])-1] - else: - z = 0 - nb_found = False - temp_ = temp - while not nb_found: - try: - temp_ = temp[:len(temp)-z] - nb = float(temp_) - nb_found = True - temp = temp_ - except ValueError: - z += 1 - continue - - if debug_arduino: - print ('filtered from COM port: ', temp) - - result.append(float(temp)) - return (result) diff --git a/DAQ_6510.py b/DAQ_6510.py deleted file mode 100644 index d0615e5..0000000 --- a/DAQ_6510.py +++ /dev/null @@ -1,736 +0,0 @@ -import serial -import pyvisa -import numpy as np -import random - -global ser_daq, debug_daq, test_daq, reading_str, nb_reading_values, ch_list, sensor_list, type_list, \ - daq_sensor_types, nplc -global ch_list_temp, ch_list_TC_K, ch_list_TC_J, ch_list_ohm, ch_list_PT_100, \ - ch_list_PT_1000, ch_list_DCV_Volt, ch_list_DCV_100mV - - -########################################################################### -def init_daq(com, rate, bits, stop, parity, dbg, test, time): -########################################################################### -### Init the interface - - global ser_daq, debug_daq, test_daq, time_daq, nplc, interface - - debug_daq = dbg - test_daq = test - time_daq = time - - if com != 'LAN': - interface = 'COM' - portName = 'COM'+str(com) - try: - serial.Serial(port=portName) - except serial.SerialException: - print ('Port ' + portName + ' not present') - if not test_daq: - quit() - - if not test_daq: - ser_daq = serial.Serial( - port = portName, - baudrate = int(rate), - stopbits = int(stop), - bytesize = int(bits), - parity = parity, - timeout = 2.0) #2.0 - else: - interface = 'LAN' - rm = pyvisa.ResourceManager() - ser_daq = rm.open_resource('daq') - - - -########################################################################### -def get_daq(): -########################################################################### -### get the measurements from the active channels - - cm_list = list(range(0)) - cm_list.append('TRAC:CLE\n') - cm_list.append('TRAC:POIN 100\n') - cm_list.append('ROUT:SCAN:CRE ' + reading_str + '\n') - cm_list.append('ROUT:SCAN:COUN:SCAN 1\n') - cm_list.append('INIT\n') - cm_list.append('*WAI\n') - - - readings = '1,' + str(nb_reading_values) + ',' - - if not time_daq: - # reading returns a comma separated string: ch,value,ch,value,... - cm_list.append('TRAC:DATA? '+ readings + '"defbuffer1", CHAN, READ\n') - else: - # reading returns a comma separated string: ch,tst,value,ch,tst,value,... - cm_list.append('TRAC:DATA? '+ readings + '"defbuffer1", CHAN, REL, READ\n') - - - if not test_daq: - for i in range(len(cm_list)): - if debug_daq: - print ('writing to COM port: ', cm_list[i]) - if interface == 'LAN': - ser_daq.write(cm_list[i]) - else: - ser_daq.write(cm_list[i].encode()) - if interface == 'LAN': - instrument = ser_daq.read() - else: - instrument = ser_daq.readline().decode() - - if debug_daq: - print ('reading from COM : ' + str(instrument)) - else: - instrument = '' - for i in range(nb_reading_values): - val = random.uniform(20,22) - instrument = instrument + ch_list[i] + ',' + str(val) + ',' - return (instrument[:-1]) - - -########################################################################### -def config_daq(ch, alias, s, t, types, nplc, PT1000mode, lsync, ocom, azer, adel): -########################################################################### -### Configure the measurement channels -### ch . channel list -### s : sensor list -### t : type of each sensor or each range -### types : sensor types -### nplc : nplc for each channel - global reading_str, nb_reading_values, ch_list, alias_list, sensor_list, type_list, \ - daq_sensor_types, daq_nplc_list, PT_1000_mode, daq_lsync, daq_ocom, daq_azer, daq_adel - global ch_list_temp, ch_list_TC_K, ch_list_TC_J, ch_list_ohm, ch_list_PT_100, \ - ch_list_PT_1000, ch_list_DCV_Volt, ch_list_DCV_100mV, ch_list_ACV - - ch_nb_TC = list(range(0)) - ch_nb_PT_100 = list(range(0)) - ch_nb_PT_1000 = list(range(0)) - ch_nb_DCV_Volt = list(range(0)) - ch_nb_DCV_100mV = list(range(0)) - ch_nb_ACV = list(range(0)) - ch_nb_Rogowski = list(range(0)) - nplc_TE = list(range(0)) - nplc_PT_100 = list(range(0)) - nplc_PT_1000 = list(range(0)) - nplc_DCV_Volt = list(range(0)) - nplc_DCV_100mV = list(range(0)) - nplc_Rogowski = list(range(0)) - PT_1000_mode = PT1000mode - ch_list = ch - alias_list = alias - sensor_list = s - type_list = t - daq_sensor_types = types - daq_nplc_list = nplc - if lsync: - daq_lsync = 'ON, ' - else: - daq_lsync = 'OFF, ' - if ocom: - daq_ocom = 'ON, ' - else: - daq_ocom = 'OFF, ' - if azer: - daq_azer = 'ON, ' - else: - daq_azer = 'OFF, ' - if adel: - daq_adel = 'ON, ' - else: - daq_adel = 'OFF, ' - - - #this will be the channel string for the measurements - reading_str = '(@' - for i in range(len(ch)): - reading_str = reading_str + ch[i] + ',' - reading_str = reading_str[:-1] + ')' - print ('reading string : ', reading_str) - nb_reading_values = len(ch) - print ('number of DAQ readings = ', nb_reading_values) - - ch_list_temp = '(@' - ch_list_TC_K = '(@' - ch_list_TC_J = '(@' - ch_list_ohm = '(@' - ch_list_PT_100 = '(@' - ch_list_PT_1000 = '(@' - ch_list_DCV_100mV = '(@' - ch_list_DCV_Volt = '(@' - ch_list_ACV = '(@' - ch_list_Rogowski = '(@' - range_list_DCV = list(range(0)) - range_list_Rogowski = list(range(0)) - range_list_ACV = list(range(0)) - nb_K = 0 - nb_J = 0 - nb_PT100 = 0 - nb_PT1000 = 0 - nb_DCV_100mV = 0 - nb_DCV_Volt = 0 - nb_ACV = 0 - nb_Rogowski = 0 - - # generate the Keithley channels and range lists - for i in range(len(ch)): - if s[i] == 'TE': - ch_list_temp = ch_list_temp + ch[i] + ',' - ch_nb_TC.append(ch[i]) - nplc_TE.append(daq_nplc_list[i]) - if t[i] == 'K': - nb_K += 1 - ch_list_TC_K = ch_list_TC_K + ch[i] + ',' - if t[i] == 'J': - nb_J += 1 - ch_list_TC_J = ch_list_TC_J + ch[i] + ',' - if s[i] == 'PT': - ch_list_ohm = ch_list_ohm + ch[i] + ',' - if t[i] == '100': - nb_PT100 += 1 - ch_list_PT_100 = ch_list_PT_100 + ch[i] + ',' - ch_nb_PT_100.append(ch[i]) - nplc_PT_100.append(daq_nplc_list[i]) - if t[i] == '1000': - nb_PT1000 += 1 - ch_list_PT_1000 = ch_list_PT_1000 + ch[i] + ',' - ch_nb_PT_1000.append(ch[i]) - nplc_PT_1000.append(daq_nplc_list[i]) - if s[i] == 'DCV-100mV': - ch_list_DCV_100mV = ch_list_DCV_100mV + ch[i] + ',' - ch_nb_DCV_100mV.append(ch[i]) - nplc_DCV_100mV.append(daq_nplc_list[i]) - nb_DCV_100mV += 1 - if s[i] == 'DCV': - ch_list_DCV_Volt = ch_list_DCV_Volt + ch[i] + ',' - ch_nb_DCV_Volt.append(ch[i]) - nplc_DCV_Volt.append(daq_nplc_list[i]) - range_list_DCV.append(t[i]) - nb_DCV_Volt += 1 - if s[i] == 'ACV': - ch_list_ACV = ch_list_ACV + ch[i] + ',' - ch_nb_ACV.append(ch[i]) - range_list_ACV.append(t[i]) - nb_ACV += 1 - if s[i] == 'Rogowski': - ch_list_Rogowski = ch_list_Rogowski + ch[i] + ',' - ch_nb_Rogowski.append(ch[i]) - nplc_Rogowski.append(daq_nplc_list[i]) - range_list_Rogowski.append(t[i]) - nb_Rogowski += 1 - - - ch_list_temp = ch_list_temp[:-1] + ')' - ch_list_TC_K = ch_list_TC_K[:-1] + ')' - ch_list_TC_J = ch_list_TC_J[:-1] + ')' - ch_list_ohm = ch_list_ohm[:-1] + ')' - ch_list_PT_100 = ch_list_PT_100[:-1] + ')' - ch_list_PT_1000 = ch_list_PT_1000[:-1] + ')' - ch_list_DCV_100mV = ch_list_DCV_100mV[:-1] + ')' - ch_list_DCV_Volt = ch_list_DCV_Volt[:-1] + ')' - ch_list_ACV = ch_list_ACV[:-1] + ')' - ch_list_Rogowski = ch_list_Rogowski[:-1] + ')' - print ('Keithley NPLC(s): ', daq_nplc_list) - print ('Keithley sensor list: ', sensor_list) - print ('Keithley range list: ', type_list) - print ('Keithley TE channel list: ', ch_list_temp) - print ('Keithley TE - K channel list: ', ch_list_TC_K) - print ('Keithley TE - J channel list: ', ch_list_TC_J) - print ('Keithley PT channel list: ', ch_list_ohm) - print ('Keithley PT-100 channel list: ', ch_list_PT_100) - print ('Keithley PT-1000 channel list: ', ch_list_PT_1000) - print ('Keithley DCV-100mV channel list: ', ch_list_DCV_100mV) - print ('Keithley DCV-Volt channel list: ', ch_list_DCV_Volt) - print ('Keithley Rogowski channel list: ', ch_list_Rogowski) - print ('Keithley ACV channel list: ', ch_list_ACV) - - cm_list = list(range(0)) - cm_list.append(':SYSTEM:CLEAR\n') - cm_list.append('FORM:DATA ASCII\n') - - if nb_K !=0 or nb_J != 0: - cm_list.append('FUNC "TEMP", ' + ch_list_temp + '\n') - cm_list.append('TEMP:TRAN TC, ' + ch_list_temp + '\n') - if nb_K != 0: - cm_list.append('TEMP:TC:TYPE K, ' + ch_list_TC_K + '\n') - if nb_J != 0: - cm_list.append('TEMP:TC:TYPE J, ' + ch_list_TC_J + '\n') - cm_list.append('TEMP:UNIT CELS, ' + ch_list_temp + '\n') - # cm_list.append('TEMP:TC:RJUN:RSEL INT, ' + ch_list_temp + '\n') - cm_list.append('TEMP:TC:RJUN:RSEL SIM, ' + ch_list_temp + '\n') - cm_list.append('TEMP:TC:RJUN:SIM 0, ' + ch_list_temp + '\n') - - cm_list.append('TEMP:AVER OFF, ' + ch_list_temp + '\n') - cm_list.append('TEMP:LINE:SYNC ' + daq_lsync + ch_list_temp + '\n') - cm_list.append('TEMP:OCOM ' + daq_ocom + ch_list_temp + '\n') - cm_list.append('TEMP:AZER ' + daq_azer + ch_list_temp + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('TEMP:DEL:AUTO ' + daq_adel + ch_list_temp + '\n') - - for i in range(nb_K+nb_J): - str_nplc = str(nplc_TE[i]) - c = '(@' + ch_nb_TC[i] + ')\n' - cm_list.append('TEMP:NPLC ' + str_nplc + ' , ' + c) - - if nb_PT100 != 0: - ## note: PT-100 can be read out as temperature sensor - cm_list.append('FUNC "TEMP", ' + ch_list_PT_100 + '\n') - cm_list.append('TEMP:TRAN FRTD, '+ ch_list_PT_100 + '\n') - cm_list.append('TEMP:RTD:FOUR PT100, ' + ch_list_PT_100 + '\n') - cm_list.append('TEMP:LINE:SYNC ' + daq_lsync + ch_list_PT_100 + '\n') - cm_list.append('TEMP:OCOM ' + daq_ocom + ch_list_PT_100 + '\n') - cm_list.append('TEMP:AZER ' + daq_azer + ch_list_PT_100 + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('TEMP:DEL:AUTO ' + daq_adel + ch_list_PT_100 + '\n') - for i in range(nb_PT100): - str_nplc = str(nplc_PT_100[i]) - c = '(@' + ch_nb_PT_100[i] + ')\n' - cm_list.append('TEMP:NPLC ' + str_nplc + ' , ' + c) - - if nb_PT1000 != 0: - # PT-1000 as 4 wire resistance - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cm_list.append('FUNC "FRES", ' + ch_list_PT_1000 + '\n') - cm_list.append('FRES:RANG 10e3, ' + ch_list_PT_1000 + '\n') - cm_list.append('FRES:LINE:SYNC ' + daq_lsync + ch_list_PT_1000 + '\n') - cm_list.append('FRES:OCOM ' + daq_ocom + ch_list_PT_1000 + '\n') - cm_list.append('FRES:AZER ' + daq_azer + ch_list_PT_1000 + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('FRES:DEL:AUTO ' + daq_adel + ch_list_PT_1000 + '\n') - cm_list.append('FRES:AVER OFF, ' + ch_list_PT_1000 + '\n') - for i in range(nb_PT1000): - str_nplc = str(nplc_PT_1000[i]) - c = '(@' + ch_nb_PT_1000[i] + ')\n' - cm_list.append('FRES:NPLC ' + str_nplc + ' , ' + c) - #PT1000 calculated inside DAQ - if PT_1000_mode == 'T': - cm_list.append('FUNC "TEMP", ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:TRAN FRTD, '+ ch_list_PT_1000 + '\n') - cm_list.append('TEMP:RTD:FOUR USER, ' + ch_list_PT_1000 + '\n') - - cm_list.append('TEMP:RTD:ALPH 0.00385055, ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:RTD:BETA 0.10863, ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:RTD:DELT 1.4999, ' + ch_list_PT_1000 + '\n') - - # neu ?? - #cm_list.append('TEMP:RTD:ALPH 0.0039022, ' + ch_list_PT_1000 + '\n') - #cm_list.append('TEMP:RTD:BETA 0.0, ' + ch_list_PT_1000 + '\n') - #cm_list.append('TEMP:RTD:DELT 0.148659, ' + ch_list_PT_1000 + '\n') - - cm_list.append('TEMP:RTD:ZERO 1000, ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:LINE:SYNC ' + daq_lsync + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:OCOM ' + daq_ocom + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:AZER ' + daq_azer + ch_list_PT_1000 + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('TEMP:DEL:AUTO ' + daq_adel + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:AVER OFF, ' + ch_list_PT_1000 + '\n') - for i in range(nb_PT1000): - str_nplc = str(nplc_PT_1000[i]) - c = '(@' + ch_nb_PT_1000[i] + ')\n' - cm_list.append('TEMP:NPLC ' + str_nplc + ' , ' + c) - - - if nb_DCV_100mV != 0: - cm_list.append('FUNC "VOLT:DC", ' + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:RANG 100e-3, ' + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:LINE:SYNC ' + daq_lsync + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:AZER ' + daq_azer + ch_list_DCV_100mV + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('VOLT:AVER OFF, ' + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:DEL:AUTO ' + daq_adel + ch_list_DCV_100mV + '\n') - for i in range(nb_DCV_100mV): - str_nplc = str(nplc_DCV_100mV[i]) - c = '(@' + ch_nb_DCV_100mV[i] + ')\n' - cm_list.append('VOLT:NPLC ' + str_nplc + ', ' + c) - - - if nb_DCV_Volt != 0: - cm_list.append('FUNC "VOLT:DC", ' + ch_list_DCV_Volt + '\n') - cm_list.append('VOLT:LINE:SYNC ' + daq_lsync + ch_list_DCV_Volt + '\n') - cm_list.append('VOLT:AZER ' + daq_azer + ch_list_DCV_Volt + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('VOLT:AVER OFF, ' + ch_list_DCV_Volt + '\n') - cm_list.append('VOLT:DEL:AUTO ' + daq_adel + ch_list_DCV_Volt + '\n') - for i in range(nb_DCV_Volt): - str_nplc = str(nplc_DCV_Volt[i]) - c = '(@' + ch_nb_DCV_Volt[i] + ')\n' - cm_list.append('VOLT:NPLC ' + str_nplc + ', ' + c) - l = len(range_list_DCV[i]) - if range_list_DCV[i][l-1:] == 'V': - r = range_list_DCV[i][:-1] - cm_list.append('VOLT:RANG ' + r + ', ' + c) - else: - #Auto - cm_list.append('VOLT:RANG:AUTO ON, ' + c) - - if nb_ACV != 0: - cm_list.append('FUNC "VOLT:AC", ' + ch_list_ACV + '\n') - cm_list.append('VOLT:AC:AVER OFF, ' + ch_list_ACV + '\n') - cm_list.append('VOLT:AC:DEL:AUTO ' + daq_adel + ch_list_ACV + '\n') - - for i in range(nb_ACV): - c = '(@' + ch_nb_ACV[i] + ')\n' - l = len(range_list_ACV[i]) - if range_list_ACV[i][l-1:] == 'V': - r = range_list_ACV[i][:-1] - cm_list.append('VOLT:AC:RANG ' + r + ', ' + c) - else: - #Auto - cm_list.append('VOLT:AC:RANG:AUTO ON, ' + c) - #cm_list.append('VOLT:AC:RANG:AUTO ON, ' + ch_list_ACV + '\n') - #cm_list.append('VOLT:AC:RANG 10, ' + ch_list_ACV + '\n') - # only signals with frequency greater than the detector bandwidth are measured - # detectors bandwith: 3, 30 or 300 Hz, default = 3 - cm_list.append('VOLT:AC:DET:BAND 300, ' + ch_list_ACV + '\n') - - if nb_Rogowski != 0: - cm_list.append('FUNC "VOLT:DC", ' + ch_list_Rogowski + '\n') - cm_list.append('VOLT:LINE:SYNC ' + daq_lsync + ch_list_Rogowski + '\n') - cm_list.append('VOLT:AZER ' + daq_azer + ch_list_Rogowski + '\n') - #if not azer: - #cm_list.append('AZER:ONCE' + '\n') - cm_list.append('VOLT:DEL:AUTO ' + daq_adel + ch_list_Rogowski + '\n') - cm_list.append('VOLT:AVER OFF, ' + ch_list_Rogowski + '\n') - for i in range(nb_Rogowski): - str_nplc = str(nplc_Rogowski[i]) - c = '(@' + ch_nb_Rogowski[i] + ')\n' - cm_list.append('VOLT:NPLC ' + str_nplc + ', ' + c) - l = len(range_list_Rogowski[i]) - if range_list_Rogowski[i][l-1:] == 'V': - r = range_list_Rogowski[i][:-1] - cm_list.append('VOLT:RANG ' + r + ', ' + c) - else: - #Auto - r = range_list_Rogowski[i] - cm_list.append('VOLT:RANG:AUTO ON, ' + ch_list_Rogowski + '\n') - - cm_list.append('DISP:CLE\n') - cm_list.append('DISP:LIGH:STAT ON50\n') - #cm_list.append('DISP:SCR HOME_LARG\n') - #cm_list.append('DISP:SCR PROC\n') - cm_list.append('DISP:USER1:TEXT "ready to start ..."\n') - #cm_list.append('DISP:BUFF:ACT "defbuffer1"\n') - #cm_list.append('ROUTE:CHAN:CLOSE (@101)\n') - #cm_list.append('DISP:WATC:CHAN (@101)\n') - - if not test_daq: - for i in range(len(cm_list)): - if debug_daq: - print ('writing to COM port: ', cm_list[i]) - if interface == 'LAN': - ser_daq.write(cm_list[i]) - else: - ser_daq.write(cm_list[i].encode()) - - -########################################################################### -def Write_LSYNC(u, state): -########################################################################### - if state == True: - onoff = 'ON, ' - else: - onoff = 'OFF, ' - if daq_sensor_types[u] == 'TE': - cmd = 'TEMP:LINE:SYNC '+ onoff + ch_list_temp + '\n' - if daq_sensor_types[u] == 'PT-100': - cmd = 'TEMP:LINE:SYNC ' + onoff + ch_list_PT_100 + '\n' - if daq_sensor_types[u] == 'PT-1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cmd = 'FRES:LINE:SYNC ' + onoff + ch_list_PT_1000 + '\n' - if PT_1000_mode == 'T': - cmd = 'TEMP:LINE:SYNC ' + onoff + ch_list_PT_1000 + '\n' - if daq_sensor_types[u] == 'DCV-100mV': - cmd = 'VOLT:LINE:SYNC ' + onoff + ch_list_DCV_100mV + '\n' - if daq_sensor_types[u] == 'DCV': - cmd = 'VOLT:LINE:SYNC ' + onoff + ch_list_DCV_Volt + '\n' - if daq_sensor_types[u] == 'Rogowski': - cmd = 'VOLT:LINE:SYNC ' + onoff + ch_list_Rogowski + '\n' - if not test_daq: - if debug_daq: - print ('Sending to COM: ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - else: - ser_daq.write(cmd.encode()) - print ('LSYNC ' + daq_sensor_types[u] + ' : ', state) - -########################################################################### -def Write_OCOM(u, state): -########################################################################### - if state == True: - onoff = 'ON, ' - else: - onoff = 'OFF, ' - if daq_sensor_types[u] == 'TE': - cmd = 'TEMP:OCOM '+ onoff + ch_list_temp + '\n' - if daq_sensor_types[u] == 'PT-100': - cmd = 'TEMP:OCOM ' + onoff + ch_list_PT_100 + '\n' - if daq_sensor_types[u] == 'PT-1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cmd = 'FRES:OCOM ' + onoff + ch_list_PT_1000 + '\n' - if PT_1000_mode == 'T': - cmd = 'TEMP:OCOM ' + onoff + ch_list_PT_1000 + '\n' - if not test_daq: - if debug_daq: - print ('Sending to COM: ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - else: - ser_daq.write(cmd.encode()) - print ('OCOM ' + daq_sensor_types[u] + ' : ', state) - - -########################################################################### -def Write_NPLC(u, val): -########################################################################### - cm_list = list(range(0)) - val_str = str(val) - if sensor_list[u] == 'TE': - cm_list.append('TEMP:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if sensor_list[u] == 'PT' and type_list[u] == '100': - cm_list.append('TEMP:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if sensor_list[u] == 'PT' and type_list[u] == '1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cm_list.append('FRES:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if PT_1000_mode == 'T': - cm_list.append('TEMP:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if sensor_list[u] == 'DCV-100mV': - cm_list.append('VOLT:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if sensor_list[u] == 'DCV': - cm_list.append('VOLT:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if sensor_list[u] == 'Rogowski': - cm_list.append('VOLT:NPLC ' + val_str + ', (@' + ch_list[u] + ')\n') - if not test_daq: - for i in range(len(cm_list)): - if debug_daq: - print ('Sending to COM: ' + cm_list[i]) - if interface == 'LAN': - ser_daq.write(cm_list[i]) - else: - ser_daq.write(cm_list[i].encode()) - print ('NPLC ' + ch_list[u] + ' (' + alias_list[u] + ') : ' + val_str) - - -########################################################################### -def Write_Filter_Count(u, val): -########################################################################### - cm_list = list(range(0)) - val_str = str(val) - if daq_sensor_types[u] == 'TE': - cm_list.append('TEMP:AVER:COUNT ' + val_str + ', ' + ch_list_temp + '\n') - if daq_sensor_types[u] == 'PT-100': - cm_list.append('TEMP:AVER:COUNT ' + val_str + ch_list_PT_100 + '\n') - if daq_sensor_types[u] == 'PT-1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cm_list.append('FRES:AVER:COUNT ' + val_str + ch_list_PT_1000 + '\n') - if PT_1000_mode == 'T': - cm_list.append('TEMP:AVER:COUNT ' + val_str + ch_list_PT_1000 + '\n') - if daq_sensor_types[u] == 'DCV-100mV': - cm_list.append('VOLT:AVER:COUNT ' + val_str + ch_list_DCV_100mV + '\n') - if daq_sensor_types[u] == 'DCV': - cm_list.append('VOLT:AVER:COUNT ' + val_str + ch_list_DCV_Volt + '\n') - if daq_sensor_types[u] == 'Rogowski': - cm_list.append('VOLT:AVER:COUNT ' + val_str + ch_list_Rogowski + '\n') - if not test_daq: - for j in range(len(cm_list)): - if debug_daq: - print ('Sending to COM: ' + cm_list[j]) - if interface == 'LAN': - ser_daq.write(cm_list[j]) - else: - ser_daq.write(cm_list[j].encode()) - print ('Filter count(s) ' + daq_sensor_types[u] + ' : ' + val_str) - -########################################################################### -def Write_Filter_State(u, state): -########################################################################### - cm_list = list(range(0)) - if daq_sensor_types[u] == 'TE': - if state == 'OFF': - cm_list.append('TEMP:AVER OFF, ' + ch_list_temp + '\n') - elif state == 'repeat': - cm_list.append('TEMP:AVER ON, ' + ch_list_temp + '\n') - cm_list.append('TEMP:AVER:TCON REP, ' + ch_list_temp + '\n') - elif state == 'moving': - cm_list.append('TEMP:AVER ON, ' + ch_list_temp + '\n') - cm_list.append('TEMP:AVER:TCON MOV, ' + ch_list_temp + '\n') - if daq_sensor_types[u] == 'PT-100': - if state == 'OFF': - cm_list.append('TEMP:AVER OFF, ' + ch_list_PT_100 + '\n') - elif state == 'repeat': - cm_list.append('TEMP:AVER ON, ' + ch_list_PT_100 + '\n') - cm_list.append('TEMP:AVER:TCON REP, ' + ch_list_PT_100 + '\n') - elif state == 'moving': - cm_list.append('TEMP:AVER ON, ' + ch_list_PT_100 + '\n') - cm_list.append('TEMP:AVER:TCON MOV, ' + ch_list_PT_100 + '\n') - if daq_sensor_types[u] == 'PT-1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - if state == 'OFF': - cm_list.append('FRES:AVER OFF, ' + ch_list_PT_1000 + '\n') - elif state == 'repeat': - cm_list.append('FRES:AVER ON, ' + ch_list_PT_1000 + '\n') - cm_list.append('FRES:AVER:TCON REP, ' + ch_list_PT_1000 + '\n') - elif state == 'moving': - cm_list.append('FRES:AVER ON, ' + ch_list_PT_1000 + '\n') - cm_list.append('FRES:AVER:TCON MOV, ' + ch_list_PT_1000 + '\n') - if PT_1000_mode == 'T': - if state == 'OFF': - cm_list.append('TEMP:AVER OFF, ' + ch_list_PT_1000 + '\n') - elif state == 'repeat': - cm_list.append('TEMP:AVER ON, ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:AVER:TCON REP, ' + ch_list_PT_1000 + '\n') - elif state == 'moving': - cm_list.append('TEMP:AVER ON, ' + ch_list_PT_1000 + '\n') - cm_list.append('TEMP:AVER:TCON MOV, ' + ch_list_PT_1000 + '\n') - if daq_sensor_types[u] == 'DCV-100mV': - if state == 'OFF': - cm_list.append('VOLT:AVER OFF, ' + ch_list_DCV_100mV + '\n') - elif state == 'repeat': - cm_list.append('VOLT:AVER ON, ' + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:AVER:TCON REP, ' + ch_list_DCV_100mV + '\n') - elif state == 'moving': - cm_list.append('VOLT:AVER ON, ' + ch_list_DCV_100mV + '\n') - cm_list.append('VOLT:AVER:TCON MOV, ' + ch_list_DCV_100mV + '\n') - if daq_sensor_types[u] == 'DCV-Volt': - if state == 'OFF': - cm_list.append('VOLT:AVER OFF, ' + ch_list_DCV_Volt + '\n') - elif state == 'repeat': - cm_list.append('VOLT:AVER ON, ' + ch_list_DCV_Volt + '\n') - cm_list.append('VOLT:AVER:TCON REP, ' + ch_list_DCV_Volt + '\n') - elif state == 'moving': - cm_list.append('VOLT:AVER ON, ' + ch_list_DCV_Volt + '\n') - cm_list.append('VOLT:AVER:TCON MOV, ' + ch_list_DCV_Volt + '\n') - if daq_sensor_types[u] == 'Rogowski': - if state == 'OFF': - cm_list.append('VOLT:AVER OFF, ' + ch_list_Rogowski + '\n') - elif state == 'repeat': - cm_list.append('VOLT:AVER ON, ' + ch_list_Rogowski + '\n') - cm_list.append('VOLT:AVER:TCON REP, ' + ch_list_Rogowski + '\n') - elif state == 'moving': - cm_list.append('VOLT:AVER ON, ' + ch_list_Rogowski + '\n') - cm_list.append('VOLT:AVER:TCON MOV, ' + ch_list_Rogowski + '\n') - - if not test_daq: - for j in range(len(cm_list)): - if debug_daq: - print ('Sending to COM: ' + cm_list[j]) - if interface == 'LAN': - ser_daq.write(cm_list[j]) - else: - ser_daq.write(cm_list[j].encode()) - print ('Filter state ' + daq_sensor_types[u] + ' : ' + state) - -########################################################################### -def Write_AZER(u, state): -########################################################################### - if state == True: - onoff = 'ON, ' - else: - onoff = 'OFF, ' - if daq_sensor_types[u] == 'TE': - cmd = 'TEMP:AZER '+ onoff + ch_list_temp + '\n' - if daq_sensor_types[u] == 'PT-100': - cmd = 'TEMP:AZER ' + onoff + ch_list_PT_100 + '\n' - if daq_sensor_types[u] == 'PT-1000': - if PT_1000_mode == 'R' or PT_1000_mode == 'R+T': - cmd = 'FRES:AZER ' + onoff + ch_list_PT_1000 + '\n' - if PT_1000_mode == 'T': - cmd = 'TEMP:AZER ' + onoff + ch_list_PT_1000 + '\n' - if daq_sensor_types[u] == 'DCV-100mV': - cmd = 'VOLT:AZER ' + onoff + ch_list_DCV_100mV + '\n' - if daq_sensor_types[u] == 'DCV': - cmd = 'VOLT:AZER ' + onoff + ch_list_DCV_Volt+ '\n' - if daq_sensor_types[u] == 'Rogowski': - cmd = 'VOLT:AZER ' + onoff + ch_list_Rogowski+ '\n' - if not test_daq: - if debug_daq: - print ('Sending to COM: ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - else: - ser_daq.write(cmd.encode()) - print ('AZER ' + daq_sensor_types[u] + ' : ', state) - - - -def reset_daq(): - print ('Reset DAQ 6510') - cm_list = list(range(0)) - cm_list.append('*RST\n') - cm_list.append('DISP:USER1:TEXT "ready to start ..."\n') - if not test_daq: - for i in range(len(cm_list)): - if debug_daq: - print ('writing to COM : ' + cm_list[i]) - if interface == 'LAN': - ser_daq.write(cm_list[i]) - else: - ser_daq.write(cm_list[i].encode()) - -def message_daq_display(): - cm_list = list(range(0)) - cm_list.append('DISP:USER1:TEXT "sampling ..."\n') - if not test_daq: - for i in range(len(cm_list)): - if debug_daq: - print ('writing to COM : ' + cm_list[i]) - if interface == 'LAN': - ser_daq.write(cm_list[i]) - else: - ser_daq.write(cm_list[i].encode()) - -def idn_daq(): - cmd = '*IDN?\n' - if not test_daq: - if debug_daq: - print ('writing to COM : ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - instrument = ser_daq.read() - else: - ser_daq.write(cmd.encode()) - instrument = ser_daq.readline().decode() - if debug_daq: - print ('reading from COM : ' , instrument) - print ('IDN: ', instrument) - -def idn_card_1(): - cmd = 'SYST:CARD1:IDN?\n' - if not test_daq: - if debug_daq: - print ('writing to COM : ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - instrument = ser_daq.read() - else: - ser_daq.write(cmd.encode()) - instrument = ser_daq.readline().decode() - if debug_daq: - print ('reading from COM : ', instrument) - print ('Card-01: ', instrument) - -def idn_card_2(): - cmd = 'SYST:CARD2:IDN?\n' - if not test_daq: - if debug_daq: - print ('writing to COM : ' + cmd) - if interface == 'LAN': - ser_daq.write(cmd) - instrument = ser_daq.read() - else: - ser_daq.write(cmd.encode()) - instrument = ser_daq.readline().decode() - if debug_daq: - print ('reading from COM : ', instrument) - print ('Card-02: ', instrument) - - diff --git a/Pyrometer.py b/Pyrometer.py deleted file mode 100644 index b96514c..0000000 --- a/Pyrometer.py +++ /dev/null @@ -1,163 +0,0 @@ -import serial -import random -import numpy - -global ser_py, debug_pyro, test_pyro - - - -########################################################################### -def Init_Pyro(i, com, rate, bits, stop, parity, dbg, test): -########################################################################### - - global ser_py, debug_pyro, test_pyro - - debug_pyro = dbg - test_pyro = test - if i == 0: - ser_py = list(range(0)) - portName = 'COM'+str(com) - try: - serial.Serial(port=portName) - except serial.SerialException: - print ('Port ' + portName + ' not present') - if not test_pyro: - quit() - - if not test_pyro: - ser_py_ = serial.Serial( - port = portName, - baudrate = int(rate), - parity = parity, - stopbits = int(stop), - bytesize = int(bits), - timeout = 0.1) - - ser_py.append(ser_py_) - - - -########################################################################### -def Config_Pyro(i, em, tr): -########################################################################### - - if not test_pyro: - print ('Pyrometer ', i+1, ' : ', Get_ID(i)) - Write_Pilot(i, False) - Write_Pyro_Para(i, 'e', str(em)) - Write_Pyro_Para(i, 't', str(tr)) - -########################################################################### -def Get_Focus(i): -########################################################################### - - p = '00df\r' - ser_py[i].write(p.encode()) - pyro_focus = ser_py[i].readline().decode() - return (pyro_focus) - -########################################################################### -def Get_ID(i): -########################################################################### - - p = '00na\r' - ser_py[i].write(p.encode()) - pyro_id = ser_py[i].readline().decode() - return (pyro_id) - - -########################################################################### -def Get_OK(i): -########################################################################### - - answer = ser_py[i].readline().decode() - print ('Pyrometer ', str(i+1), ' = ', answer) - - -########################################################################### -def Write_Pyro_Para(i, para, str_val): -########################################################################### -### e = emission, t = transmission - - if para == 'e': - val = '%05.1f' % float(str_val) - str_val = str(val).replace('.', '') - p = '00em' + str_val + '\r' - if para == 't': - val = '%05.1f' % float(str_val) - str_val = str(val).replace('.', '') - p = '00et' + str_val + '\r' - if para == 't90': - p = '00ez' + str_val + '\r' - if not test_pyro: - if debug_pyro: - print ('Sending to ' + ser_py[i].port +': ', p.encode()) - ser_py[i].write(p.encode()) - Get_OK(i) - answer = Get_Pyro_Para(i, para) - if para == 'e': - print ('Pyrometer ', str(i+1), ' emission = ', answer) - if para == 't': - print ('Pyrometer ', str(i+1), ' transmission = ', answer) - if para == 't90': - print ('Pyrometer ', str(i+1), ' t90 = ', answer) - else: - print ('Pyro ' +str(i+1) + ' parameter: ', p) - - -########################################################################### -def Get_Pyro_Para(i, para): -########################################################################### -### e = emission, t = transmission - - if para == 'e': - p = '00em\r' - if para == 't': - p = '00et\r' - if para == 't90': - p = '00ez\r' - if not test_pyro: - if debug_pyro: - print ('Sending to ' + ser_py[i].port +': ', p.encode()) - ser_py[i].write(p.encode()) - answer = ser_py[i].readline().decode() - return (answer) - else: - print ('Pyro ' +str(i+1) + ' parameter: ', p) - - -########################################################################### -def Read_Pyro(i): -########################################################################### - - p = '00ms\r' - if not test_pyro: - if debug_pyro: - print ('Sending to ' + ser_py[i].port + ': ', p.encode()) - ser_py[i].write(p.encode()) - temp = ser_py[i].readline().decode() - temp = temp[:-1] - l = len(temp) - temp = temp[:l-1] + '.' + temp[l-1:] - if debug_pyro: - print ('Reading from ' + ser_py[i].port + ': ', float(temp)) - else: - temp = random.uniform(20,22) - return (float(temp)) - - - -########################################################################### -def Write_Pilot(i, state): -########################################################################### - - print ('Pilot-'+str(i+1)+' : ' +str(state)) - if not test_pyro: - if state: - p = '00la1\r' - else: - p = '00la0\r' - if debug_pyro: - print ('Sending to ' + ser_py[i].port +': ', p.encode()) - ser_py[i].write(p.encode()) - Get_OK(i) \ No newline at end of file diff --git a/Pyrometer_Array.py b/Pyrometer_Array.py deleted file mode 100644 index 9d51e84..0000000 --- a/Pyrometer_Array.py +++ /dev/null @@ -1,134 +0,0 @@ -import serial -import random -import numpy - -global ser_py_array, debug_pyro, test_pyro - - - -########################################################################### -def Init_Pyro_Array(com, rate, bits, stop, parity, dbg, test): -########################################################################### - - global ser_py_array, debug_pyro, test_pyro - - debug_pyro = dbg - test_pyro = test - portName = 'COM'+str(com) - try: - serial.Serial(port=portName) - except serial.SerialException: - print ('Port ' + portName + ' not present') - if not test_pyro: - quit() - - if not test_pyro: - ser_py_array = serial.Serial( - port = portName, - baudrate = int(rate), - parity = parity, - stopbits = int(stop), - bytesize = int(bits), - timeout = 0.1) - - - -########################################################################### -def Config_Pyro_Array(i, em): -########################################################################### -# i is the head number, starts with 0 - - if not test_pyro: - print ('Pyrometer array head', i+1, ' : ', Get_head_ID(i)) - Write_Pyro_Array_Para(i, 'e', str(em)) - -########################################################################### -def Get_nb_of_head(): -########################################################################### - - p = '00oc\r' - -########################################################################### -def Get_head_ID(i): -########################################################################### -# i is the head number, starts with 0 - - p = '00A' + str(i+1) + 'sn\r' - ser_py_array.write(p.encode()) - pyro_head_id = ser_py_array.readline().decode() - return (pyro_head_id) - -########################################################################### -def Get_OK(i): -########################################################################### - - answer = ser_py_array.readline().decode() - print ('Pyrometer array head', str(i+1), ' = ', answer) - - -########################################################################### -def Write_Pyro_Array_Para(i, para, str_val): -########################################################################### -### e = emission -### i is the head number, starts with 0 - - if para == 'e': - val = '%05.1f' % float(str_val) - str_val = str(val).replace('.', '') - p = '00A' + str(i+1) + 'em' + str_val + '\r' - if para == 't90': - p = '00A' + str(i+1) + 'ez' + str_val + '\r' - if not test_pyro: - if debug_pyro: - print ('Sending to head ' + str(i+1) +': ', p.encode()) - ser_py_array.write(p.encode()) - Get_OK(i) - answer = Get_Pyro_Array_Para(i, para) - if para == 'e': - print ('Pyrometer array head ', str(i+1), ' emission = ', answer) - if para == 't90': - print ('Pyrometer array head ', str(i+1), ' t90 = ', answer) - else: - print ('Pyrometer array head ' +str(i+1) + ' parameter: ', p) - - -########################################################################### -def Get_Pyro_Array_Para(i, para): -########################################################################### -### e = emission, t = transmission - - if para == 'e': - p = '00A' + str(i+1) + 'em\r' - if para == 't90': - p = '00A' + str(i+1) + 'ez\r' - if not test_pyro: - if debug_pyro: - print ('Sending to pyrometer head ' + str(i+1) + ': ', p.encode()) - ser_py_array.write(p.encode()) - answer = ser_py_array.readline().decode() - return (answer) - else: - print ('Pyrometer array head ' + str(i+1) + ' parameter: ', p) - - -########################################################################### -def Read_Pyro_Array(i): -########################################################################### -# i is the head number, starts with 0 - - p = '00A' + str(i+1) + 'ms\r' - if not test_pyro: - if debug_pyro: - print ('Sending to head ', + str(i+1) + ': ', p.encode()) - ser_py_array.write(p.encode()) - temp = ser_py_array.readline().decode() - temp = temp[:-1] - l = len(temp) - temp = temp[:l-1] + '.' + temp[l-1:] - if debug_pyro: - print ('Reading from head ' + str(i+1) + ': ', float(temp)) - else: - temp = random.uniform(20,22) - return (float(temp)) - - diff --git a/README.md b/README.md index 261b19b..83dff76 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,176 @@ # multilog +[![DOI](https://zenodo.org/badge/419782987.svg)](https://zenodo.org/badge/latestdoi/419782987) -Masurement data recording and visualization using various devices. +Measurement data recording and visualization using various devices, e.g., multimeters, pyrometers, optical or infrared cameras. -The project is developed and maintained by the [**Model experiments group**](https://www.ikz-berlin.de/en/research/materials-science/section-fundamental-description#c486) at the Leibniz Institute for Crystal Growth (IKZ). +![multilog 2](./multilog.png) + +The project is developed and maintained by the [**Model experiments group**](https://www.ikz-berlin.de/en/research/materials-science/section-fundamental-description#c486) at the Leibniz-Institute for Crystal Growth (IKZ). ### Referencing -If you use this code in your research, please cite our article (available with open access): + +If you use this code in your research, please cite our open-access article: > A. Enders-Seidlitz, J. Pal, and K. Dadzis, Model experiments for Czochralski crystal growth processes using inductive and resistive heating *IOP Conference Series: Materials Science and Engineering*, 1223 (2022) 012003. https://doi.org/10.1088/1757-899X/1223/1/012003. -## Programs -Python 3 is used. +## Supported devices + +Currently, the following devices are supported: + +- Keithley DAQ6510 multimeter (RS232) +- Lumasense pyrometers (RS485): + - IGA-6-23 + - IGAR-6 + - Series-600 +- Basler optical cameras (Ethernet) +- Optris IP-640 IR camera (USB) +- Eurotherm controller (RS232) +- IFM flowmeter (Ethernet) + +Additional devices may be included in a flexible manner. + +## Usage + +multilog is configured using the file *config.yml* in the main directory. A template [*config_template.yml*](./config_template.yml), including all supported measurement devices, is provided in this repository; please create a copy of this file and adjust it to your needs. Further details are given below. + +To run multilog execute the python file [*multilog.py*](./multilog.py): + +```shell +python3 ./multilog.py +``` + +If everything is configured correctly, the GUI window opens up. Sampling is started immediately for verification purposes, but the measurements are not recorded yet. Once the *Start* button is clicked, the directory "measdata_*date*_#*XX*" is created and samplings are saved to this directory in csv format. A separate file (or folder for images) is created for each measurement device. + +multilog is built for continuous sampling. In case of problems, check the log file for errors and warnings! + +## Configuration + +multilog is configured using the file *config.yml* in the main directory. A template [*config_template.yml*](./config_template.yml) including all supported measurement devices is provided in this repository; please create a copy of this file and adjust it to your needs. + +### Main settings + +In the *settings* section of the config-file the sampling time steps are defined (in ms): + +- dt-main: main time step for sampling once the recording is started. Used for all devices except cameras. +- dt-camera: time step for sampling of cameras. +- dt-camera-update: time step for updating camera view. This value should be lower than dt-camera (to get a smooth view) but not lower than exposure + processing time. +- dt-init: time step used for sampling before recording is started. + +### Logging + +The logging is configured in the *logging* section of the config-file. The parameters defined are passed directly to the [basicConfig-function](https://docs.python.org/3/library/logging.html#logging.basicConfig) of Python's logging module. + +### Devices + +The *devices* section is the heart of multilog's configuration and contains the settings for the measurement devices. You can add any number of supported devices here. Just give them an individual name. A separate tab will be created in the GUI for each device. The device type is defined by the name as given in *config-template.yml*, e.g. "DAQ-6510", "IFM-flowmeter", or "Optris-IP-640", and must always be contained in this name; extensions are possible (e.g., "DAQ-6510 - temperatures"). + +#### DAQ-6510 multimeter + +For the Keithley DAQ6510 multimeter, the following main settings are available: + +- serial-interface: configuration for [pyserial](https://pyserial.readthedocs.io/en/latest/pyserial_api.html#serial.Serial) +- settings: currently, some channel-specific settings are defined globally. This will be changed in the future. +- channels: flexible configuration of the device's channels for measurement of temperatures with thermocouples / Pt100 / Pt1000 and ac / dc voltages. Conversion of voltages into different units is possible (see "rogowski" in config_template.yml). + +#### IFM-flowmeter + +The main configurations (IP, ports) should be self-explaining. If the section "flow-balance" is included in the settings, in- and outflows are balanced to check for leakage. This is connected to a discord-bot for automatized notification; the bot configuration is hard-coded in [*discord_bot.py*](./multilog/discord_bot.py). + +#### Eurotherm controller + +Temperature measurement and control operation points are logged. Configuration of: -Script | Related Device ---------------------|------------------------ -Pyrometer.py | Impac IGA 6/23 and IGAR 6 Adv. -Pyrometer_Array.py | Impac Series 600 -DAQ_6510.py | Multimeter -Arduino.py | Not yet fully implemented +- serial-interface: configuration for [pyserial](https://pyserial.readthedocs.io/en/latest/pyserial_api.html#serial.Serial) -__Other files__ -1. sample.py - * The main script to start -2. config.ini - * configuration file for the used devices +#### Lumasense IGA-6-23 / IGAR-6-adv / Series-600 pyrometer -__Further modules to be integrated here__ +The configuration of the Lumasense pyrometers includes: -[IR Camera](https://github.com/nemocrys/IRCamera) +- serial-interface: configuration for [pyserial](https://pyserial.readthedocs.io/en/latest/pyserial_api.html#serial.Serial) +- device-id: RS485 device id (default: '00') +- transmissivity +- emissivity +- t90 -## Operation +#### Basler optical camera -Start the main sample.py in a command window: -python sample.py +The camera is connected using ethernet. Configuration of: -The flag --h shows some command line parameters +- device number (default: 0) +- exposure time +- framerate +- timeout + +#### Optris-IP-640 IR camera + +Configuration according to settings in [FiloCara/pyOptris](https://github.com/FiloCara/pyOptris/blob/dev/setup.py), including: + +- measurement-range +- framerate +- emissivity +- transmissivity + +## Program structure + +multilog follows the [Model-view-vontroller](https://de.wikipedia.org/wiki/Model_View_Controller) pattern. For each measurement device two classes are defined: a pyQT-QWidget "view" class for visualization in [*view.py*](./multilog/view.py) and a "model" class in [*devices.py*](./multilog/devices.py). The "controller", including program construction and main sampling loop, is defined in [*main.py*](./multilog/main.py). + +To add a new device, the following changes are required: + +- create a device-class implementing the device configuration, sampling, and saving in [*devices.py*](./multilog/devices.py) +- create a widget-class implementing the GUI in [*view.py*](./multilog/view.py) +- add the configuration in the *devices* section in [*config_template.yml*](./config_template.yml) +- add the new device to the "setup devices & tabs" section (search for "# add new devices here!") in Controller.\_\_init\_\_(...) in [*main.py*](./multilog/main.py) + +## Dependencies + +multilog runs with python >= 3.7 on both Linux and Windows (Mac not tested). The main dependencies are the following python packages: + +- matplotlib +- numpy +- PyQT5 +- pyqtgraph +- pyserial +- PyYAML + +Depending on the applied devices multilog needs various additional python packages. A missing device-specific dependency leads to a warning. Always check the log if something is not working as expected! + +#### IFM-flowmeter + +- requests + +For the discord bot there are the following additional dependencies: + +- dotenv +- discord + +#### Basler optical camera + +- pypylon +- Pillow + +#### Optris-IP-640 IR camera + +- mpl_toolkits +- pyoptris and dependencies installed according to https://github.com/nemocrys/pyOptris/blob/dev/README.md + +## NOMAD support + +NOMAD support and the option to uploade measurement data to [NOMAD](https://nomad-lab.eu/) is under implementation. Currently, various yaml-files containing a machine-readable description of the measurement data are generated. + +## License + +This code is available under a GPL v3 License. Parts are copied from [FiloCara/pyOptris](https://github.com/FiloCara/pyOptris/blob/dev/setup.py) and available under MIT License. + +## Support + +In case of questions please [open an issue](https://github.com/nemocrys/multilog/issues/new)! ## Acknowledgements [This project](https://www.researchgate.net/project/NEMOCRYS-Next-Generation-Multiphysical-Models-for-Crystal-Growth-Processes) has received funding from the European Research Council (ERC) under the European Union's Horizon 2020 research and innovation programme (grant agreement No 851768). + +## Contribution + +Any help to improve this code is very welcome! diff --git a/config.ini b/config.ini deleted file mode 100644 index 09c098d..0000000 --- a/config.ini +++ /dev/null @@ -1,97 +0,0 @@ -[Instruments] -# DAQ-parameter: on/off, COM-port, datarate, #bits, #stop, parity -# DAQ -parameter: COM=LAN optionally -# Pyro: number of pyrometer -# Pyro-Array-parameter: on/off, COM-port, datarate, #bits, #stop, parity -# Arduino: number of arduinos -DAQ-6510: on, 1, 115200, 8, 1, N -Pyro: 2 -Pyro-Array: off, 4, 115200, 8, 1, E -Arduino: 0 -[Overflow] -DAQ-6510: 2000 -Pyro: 1000 -Arduino: 999 -[PT-1000] -# options: R, T, R+T -# T : calculated intrinsic inside instrument -# R, R+T : sampled R and calculated to T by function -Save: T -[Card-01] -# up to 20 channels -# remark: for PT100 or PT1000 in 4 wire mode the actual channel -# is mapped to channel +10 (i.e.: Ch01->Ch11) -# Ch-xx: alias, sensor, type(or range), nplc, factor, offset -Ch-01: TE_1_K_left, TE, K, 0.5, 1.0, 0 -Ch-02: TE_2_K_left, TE, K, 0.5, 1.0, 0 -Ch-03: TE_3_insu_right_out, TE, K, 0.5, 1.0, 0 -#Ch-04: TE_4_insu_front_out, TE, K, 0.5, 1.0, 0 -#Ch-05: TE_5_insu_right_in, TE, K, 0.5, 1.0, 0 -#Ch-06: TE_Q_b, TE, K, 0.5, 1.0, 0 -#Ch-07: TE_Q_f, TE, K, 0.5, 1.0, 0 -#Ch-19: TE_6_air_outside, TE, J, 0.5, 1.0, 0 -Ch-20: TE_7_J_left, TE, J, 0.5, 1.0, 0 -[Card-02] -# up to 20 channels -# remark: for PT100 or PT1000 in 4 wire mode the actual channel -# is mapped to channel +10 (i.e.: Ch01->Ch11) -# Ch-xx: alias, sensor, type(or range), nplc, factor, offset -# Rogowski: 50 A/V - older one -# Rogowski: 30/300/3000 A : factor = 10/100/1000 -Ch-01: Rogowski, Rogowski, 10V, 0.5, 100, 0 -#Ch-02: Rogowski, Rogowski, 10V, 0.5, 100, 0 -Ch-06: PT-100_1_rear, PT, 100, 0.5, 1.0, 0 -#Ch-07: PT-100_2, PT, 100, 0.5, 1.0, 0 -#Ch-08: PT-1000_1_air_above, PT, 1000, 0.5, 1.0, 0 -#Ch-09: PT-1000_2, PT, 1000, 0.5, 1.0, 0 -#Ch-11: LEM [A], DCV, 10V, 0.5, 40, 0 -#Ch-12: Flux_bottom, DCV, 100mV, 0.5, 1.0, 0 -#Ch-13: Flux_front, DCV, 100mV, 0.5, 1.0, 0 - - -[Card-05] -# virtually mapped pyrometer(s) -# Ch-xx: alias, COM, Transmission, Emission, Datarate, bits, stopbit, parity, t90, factor, offset, (t90 list) -# t90: (0.002, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) (IGA 320/23 - Adrian) -# t90: (0.0005, 0.001, 0.003, 0.005, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) (IGA 6-23 Adv) -# t90: (0.0005, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) (IGAR 6 Q) -Ch-01: Pyro-1-Quo, 2, 100, 100, 115200, 8, 1, E, 2, 1.0, 0, (0.0005, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) -Ch-02: Pyro-2-Laser, 3, 53, 83, 115200, 8, 1, E, 2, 1.0, 0, (0.0005, 0.001, 0.003, 0.005, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) -#Ch-03: Pyro-3-Visier-front, 6, 100, 100, 19200, 8, 1, E, 3, 1.0, 0, (0.0005, 0.001, 0.003, 0.005, 0.01, 0.05, 0.25, 1.0, 3.0, 10.0) - -[Card-20] -# virtually mapped pyrometer array -# no transmission -# Ch-xx: alias, Emission, t90, factor, offset, (t90 list) -# t90: (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) (Series 600 Array) -Ch-01: Pyro_h1, 95, 1, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-02: Pyro_h2, 100, 1, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-03: Pyro_h3, 100, 2, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-04: Pyro_h4, 100, 1, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-05: Pyro_h5, 90, 2, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-06: Pyro_h6, 100, 2, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-07: Pyro_h7, 90, 2, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) -#Ch-08: Pyro_h8, 100, 2, 1.0, 0, (0.18, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0) - - -[Card-10] -# virtually mapped arduino -# COM: COM, Datarate, bits, stopbit, parity -# Ch-xx: alias, send-command, start-id, end-id, position, separator if position>0, , factor, offset -# remark: heating status channel (if present) at last position -COM: 10, 19200, 8, 1, N -Ch-01: Temp-Type-K-plate, g, g=, \r\n, , "", 1.0, 0 -Ch-02: Temp-IKA_2, GN_PV_2, , \r\n, 1, " ", 1.0, 0 -Ch-03: Temp-Test, HN_X, XSTART=, XEND, 2, ";", 1.0, 0 -Ch-04: Temp-IKA_1, IN_PV_1, , \r\n, 1, " ", 1.0, 0 -Ch-05: Temp-Type-K-air, c, c=, \r\n, , "", 1.0, 0 -Ch-06: Heating status [0/1], h, h=, \r\n, , "", 1.0, 0 -[Card-11] -# virtually mapped arduino -# COM: COM, Datarate, bits, stopbit, parity -# Ch-xx: alias, send-command, start-id, end-id, position, separator if position>0, , factor, offset -# remark: heating status channel (if present) at last position -COM: 17, 19200, 8, 1, N -Ch-01: Temp-Type-K-plate, g, g=, \r\n, , "", 1.0, 0 - - diff --git a/multilog.png b/multilog.png new file mode 100644 index 0000000..395b480 Binary files /dev/null and b/multilog.png differ diff --git a/multilog.py b/multilog.py new file mode 100644 index 0000000..590429b --- /dev/null +++ b/multilog.py @@ -0,0 +1,6 @@ +"""Execute this to start multilog!""" + +from multilog.main import main + +if __name__ == "__main__": + main() diff --git a/multilog/__init__.py b/multilog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/multilog/devices.py b/multilog/devices.py new file mode 100644 index 0000000..118639a --- /dev/null +++ b/multilog/devices.py @@ -0,0 +1,1478 @@ +"""This module contains a class for each device implementing +device configuration, communication and saving of measurement data. + +Each device must implement the following functions: +- init_output(self, directory: str) -> None +- sample(self) -> Any +- save_measurement(self, time_abs: float, time_rel: datetime, sampling: Any) -> None +""" + +from copy import deepcopy +import datetime +import logging +import matplotlib.pyplot as plt +import multiprocessing +import numpy as np +import os +from serial import Serial, SerialException +import subprocess +import traceback +import yaml + +logger = logging.getLogger(__name__) + +from .discord_bot import send_message + +# device-specific imports +# required by IfmFlowmeter +try: + import requests +except Exception as e: + logger.warning("Could not import requests.", exc_info=True) +# required by BaslerCamera +try: + from pypylon import pylon +except Exception as e: + logger.warning("Could not import pypylon.", exc_info=True) +# required by BaslerCamera +try: + from PIL import Image +except Exception as e: + logger.warning("Could not import PIL.", exc_info=True) +# required by OptrisIP640 +try: + from mpl_toolkits.axes_grid1 import make_axes_locatable +except Exception as e: + logger.warning("Could not import mpl_toolkits.", exc_info=True) +# required by OptrisIP640 +try: + from .pyOptris import direct_binding as optris +except Exception as e: + logger.warning(f"Could not import pyOtris", exc_info=True) + + +class SerialMock: + """This class is used to mock a serial interface for debugging purposes.""" + + def write(self, _): + pass + + def readline(self): + return "".encode() + + +class Daq6510: + """Keythley multimeter DAQ6510. Implementation bases on v1 of + multilog and shall be refactored in future.""" + + def __init__(self, config, name="Daq6510"): + """Setup serial interface, configure device and prepare sampling. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): Device name. + """ + logger.info(f"Initializing Daq6510 device '{name}'") + self.config = config + self.name = name + try: + self.serial = Serial(**config["serial-interface"]) + except SerialException as e: + logger.exception(f"Connection to {self.name} not possible.") + self.serial = SerialMock() + self.reset() + # bring the data from config into multilog v1 compatible structure + self.nb_reading_values = len(config["channels"]) + self.reading_str = "(@" + + self.ch_list_tc = [] + self.ch_list_pt100 = [] + self.ch_list_pt1000 = [] + self.ch_list_dcv = [] + self.ch_list_acv = [] + + self.ch_str_tc = "(@" + self.ch_str_tc_k = "(@" + self.ch_str_tc_j = "(@" + self.ch_str_pt_100 = "(@" + self.ch_str_pt_1000 = "(@" + self.ch_str_dcv = "(@" + self.ch_str_acv = "(@" + + self.nb_tc_k = 0 + self.nb_tc_j = 0 + self.nb_pt100 = 0 + self.nb_pt1000 = 0 + self.nb_dcv = 0 + self.nb_acv = 0 + + for channel in config["channels"]: + self.reading_str += f"{channel}," + sensor_type = config["channels"][channel]["type"].lower() + if sensor_type == "temperature": + subtype = config["channels"][channel]["sensor-id"].split("_")[0].lower() + if subtype == "te": # thermo couple + self.ch_list_tc.append(channel) + self.ch_str_tc += f"{channel}," + tc_type = ( + config["channels"][channel]["sensor-id"].split("_")[-1].lower() + ) + if tc_type == "k": + self.nb_tc_k += 1 + self.ch_str_tc_k += f"{channel}," + if tc_type == "j": + self.nb_tc_j += 1 + self.ch_str_tc_j += f"{channel}," + if subtype == "pt-100": + self.ch_list_pt100.append(channel) + self.nb_pt100 += 1 + self.ch_str_pt_100 += f"{channel}," + if subtype == "pt-1000": + self.ch_list_pt1000.append(channel) + self.nb_pt1000 += 1 + self.ch_str_pt_1000 += f"{channel}," + elif sensor_type == "dcv": + self.ch_list_dcv.append(channel) + self.nb_dcv += 1 + self.ch_str_dcv += f"{channel}," + elif sensor_type == "acv": + self.ch_list_acv.append(channel) + self.nb_acv += 1 + self.ch_str_acv += f"{channel}," + else: + raise ValueError( + f"Unknown sensor type {sensor_type} at channel {channel}." + ) + + self.reading_str = self.reading_str[:-1] + ")" + self.ch_str_tc = self.ch_str_tc[:-1] + ")" + self.ch_str_tc_k = self.ch_str_tc_k[:-1] + ")" + self.ch_str_tc_j = self.ch_str_tc_j[:-1] + ")" + self.ch_str_pt_100 = self.ch_str_pt_100[:-1] + ")" + self.ch_str_pt_1000 = self.ch_str_pt_1000[:-1] + ")" + self.ch_str_dcv = self.ch_str_dcv[:-1] + ")" + self.ch_str_acv = self.ch_str_acv[:-1] + ")" + + if config["settings"]["lsync"]: + lsync = "ON" + else: + lsync = "OFF" + if config["settings"]["ocom"]: + ocom = "ON" + else: + ocom = "OFF" + if config["settings"]["azer"]: + azer = "ON" + else: + azer = "OFF" + if config["settings"]["adel"]: + adel = "ON" + else: + adel = "OFF" + + cmds = [ + ":SYSTEM:CLEAR\n", + "FORM:DATA ASCII\n", + ] + if self.nb_tc_k + self.nb_tc_j > 0: # if there are thermo couples + cmds.append(f'FUNC "TEMP", {self.ch_str_tc}\n') + cmds.append(f"TEMP:TRAN TC, {self.ch_str_tc}\n") + if self.nb_tc_k > 0: + cmds.append(f"TEMP:TC:TYPE K, {self.ch_str_tc_k}\n") + if self.nb_tc_j > 0: + cmds.append(f"TEMP:TC:TYPE J, {self.ch_str_tc_j}\n") + cmds.append(f"TEMP:UNIT CELS, {self.ch_str_tc}\n") + if config["settings"]["internal-cold-junction"]: + cmds.append(f"TEMP:TC:RJUN:RSEL INT, {self.ch_str_tc}\n") + else: + cmds.append(f"TEMP:TC:RJUN:RSEL SIM, {self.ch_str_tc}\n") + cmds.append(f"TEMP:TC:RJUN:SIM 0, {self.ch_str_tc}\n") + cmds.append(f"TEMP:AVER OFF, {self.ch_str_tc}\n") + cmds.append(f"TEMP:LINE:SYNC {lsync}, {self.ch_str_tc}\n") + cmds.append(f"TEMP:OCOM {ocom}, {self.ch_str_tc}\n") + cmds.append(f"TEMP:AZER {azer}, {self.ch_str_tc}\n") + cmds.append(f"TEMP:DEL:AUTO {adel}, {self.ch_str_tc}\n") + for channel in self.ch_list_tc: + cmds.append(f'TEMP:NPLC {config["settings"]["nplc"]}, (@{channel})\n') + if self.nb_pt100 > 0: + cmds.append(f'FUNC "TEMP", {self.ch_str_pt_100}\n') + cmds.append(f"TEMP:TRAN FRTD, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:RTD:FOUR PT100, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:LINE:SYNC {lsync}, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:OCOM {ocom}, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:AZER {azer}, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:DEL:AUTO {adel}, {self.ch_str_pt_100}\n") + cmds.append(f"TEMP:AVER OFF, {self.ch_str_pt_100}\n") + for channel in self.ch_list_pt100: + cmds.append(f'TEMP:NPLC {config["settings"]["nplc"]}, (@{channel})\n') + if self.nb_pt1000 > 0: # PT1000 temperature calculated inside DAQ + cmds.append(f'FUNC "TEMP", {self.ch_str_pt_1000}\n') + cmds.append(f"TEMP:TRAN FRTD, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:RTD:FOUR USER, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:RTD:ALPH 0.00385055, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:RTD:BETA 0.10863, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:RTD:DELT 1.4999, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:RTD:ZERO 1000, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:LINE:SYNC {lsync}, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:OCOM {ocom}, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:AZER {azer}, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:DEL:AUTO {adel}, {self.ch_str_pt_1000}\n") + cmds.append(f"TEMP:AVER OFF, {self.ch_str_pt_1000}\n") + for channel in self.ch_list_pt1000: + cmds.append(f'TEMP:NPLC {config["settings"]["nplc"]}, (@{channel})\n') + if self.nb_dcv > 0: + cmds.append(f'FUNC "VOLT:DC", {self.ch_str_dcv}\n') + cmds.append(f"VOLT:LINE:SYNC {lsync}, {self.ch_str_dcv}\n") + cmds.append(f"VOLT:AZER {azer}, {self.ch_str_dcv}\n") + cmds.append(f"VOLT:DEL:AUTO {adel}, {self.ch_str_dcv}\n") + cmds.append(f"VOLT:AVER OFF, {self.ch_str_dcv}\n") + for channel in self.ch_list_dcv: + cmds.append(f'VOLT:NPLC {config["settings"]["nplc"]}, (@{channel})\n') + if "range" in config["channels"][channel]: + cmds.append( + f'VOLT:RANG {config["channels"][channel]["range"]}, (@{channel})\n' + ) + else: + cmds.append(f"VOLT:RANG:AUTO ON, (@{channel})\n") + if self.nb_acv > 0: + cmds.append(f'FUNC "VOLT:AC", {self.ch_str_acv}\n') + cmds.append(f"VOLT:AC:AVER OFF, {self.ch_str_acv}\n") + cmds.append(f"VOLT:AC:DEL:AUTO {adel}, {self.ch_str_acv}\n") + for channel in self.ch_list_acv: + if "range" in config["channels"][channel]: + cmds.append( + f'VOLT:AC:RANG {config["channels"][channel]["range"]}, (@{channel})\n' + ) + else: + cmds.append(f"VOLT:AC:RANG:AUTO ON, (@{channel})\n") + # only signals with frequency greater than the detector bandwidth are measured + # detectors bandwith: 3, 30 or 300 Hz, default = 3 + cmds.append(f"VOLT:AC:DET:BAND 300, {self.ch_str_acv}\n") + cmds.append("DISP:CLE\n") + cmds.append("DISP:LIGH:STAT ON50\n") + cmds.append('DISP:USER1:TEXT "ready to start ..."\n') + + for cmd in cmds: + self.serial.write(cmd.encode()) + + # container for measurement data, allocation of channel_id and name + self.meas_data = {} + self.channel_id_names = {} + for channel in config["channels"]: + if "position" in config["channels"][channel]: + name = f'{config["channels"][channel]["sensor-id"]} {config["channels"][channel]["position"]}' + else: + name = f'{config["channels"][channel]["sensor-id"]}' + name = name.replace(",", "") + self.meas_data.update({name: []}) + self.channel_id_names.update({channel: name}) + + # unit conversion (for dcv and acv channels) + self.conversion_factor = {} + self.unit = {} + for channel in config["channels"]: + type = config["channels"][channel]["type"].lower() + name = self.channel_id_names[channel] + if type == "temperature": + self.unit.update({name: "°C"}) + self.conversion_factor.update({name: 1}) + else: # acv, dcv + if "unit" in config["channels"][channel]: + self.unit.update({name: config["channels"][channel]["unit"]}) + else: + self.unit.update({name: "V"}) + if "factor" in config["channels"][channel]: + self.conversion_factor.update( + {name: config["channels"][channel]["factor"]} + ) + else: + self.conversion_factor.update({name: 1}) + + def init_output(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + units = "# datetime,s," + header = "time_abs,time_rel," + for sensor in self.meas_data: + units += f"{self.unit[sensor].replace('°', 'DEG ')}," + header += f"{sensor}," + units += "\n" + header += "\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + self.write_nomad_files(directory) + + def write_nomad_files(self, directory="./"): + """Write .archive.yaml file based on device configuration. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + with open("./multilog/nomad/archive_template.yml") as f: + nomad_template = yaml.safe_load(f) + definitions = nomad_template.pop("definitions") + data = nomad_template.pop("data") + sensor_schema_template = nomad_template.pop("sensor_schema_template") + + data.update( + { + "data_file": self.filename.split("/")[-1], + } + ) + for channel in self.channel_id_names: + sensor_name = self.channel_id_names[channel] + sensor_name_nomad = sensor_name.replace(" ", "_").replace("-", "_") + data.update( + { + sensor_name_nomad: { + # "model": "your_field_here", + "name": sensor_name_nomad, + "sensor_id": sensor_name.split(" ")[0], + "attached_to": " ".join(sensor_name.split(" ")[1:]), + "measured_property": self.config["channels"][channel]["type"], + "type": sensor_name.split("_")[0].split(" ")[0], + # "notes": "TE_1_K air 155 mm over crucible", + # "unit": self.unit[sensor_name], # TODO + # "channel": channel, # TODO + "value_timestamp_rel": "#/data/value_timestamp_rel", + "value_timestamp_abs": "#/data/value_timestamp_abs", + } + } + ) + sensor_schema = deepcopy(sensor_schema_template) + sensor_schema["section"]["quantities"]["value_log"]["m_annotations"][ + "tabular" + ]["name"] = sensor_name + definitions["sections"]["Sensors_list"]["sub_sections"].update( + {sensor_name_nomad: sensor_schema} + ) + definitions["sections"]["Sensors_list"]["m_annotations"]["plot"].append( + { + "label": f"{sensor_name} over time", + "x": "value_timestamp_rel", + "y": [f"{sensor_name_nomad}/value_log"], + } + ) + nomad_dict = { + "definitions": definitions, + "data": data, + } + with open(f"{directory}/{self.name}.archive.yaml", "w", encoding="utf-8") as f: + yaml.safe_dump(nomad_dict, f, sort_keys=False) + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to file. + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (dict): sampling data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel}," + for sensor in self.meas_data: + self.meas_data[sensor].append(sampling[sensor]) + line += f"{sampling[sensor]}," + line += "\n" + with open(self.filename, "a") as f: + f.write(line) + + @property + def device_id(self): + """Get the device ID.""" + cmd = "*IDN?\n" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + @property + def card1_id(self): + """Get the ID of Card #1.""" + cmd = "SYST:CARD1:IDN?\n" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + @property + def card2_id(self): + """Get the ID of Card #2.""" + cmd = "SYST:CARD2:IDN?\n" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + def set_display_message(self, message="hello world"): + """Set a message on the display.""" + cmd = f'DISP:USER1:TEXT "{message}"\n' + self.serial.write(cmd.encode()) + + def reset(self): + """Reset device to factory default.""" + logger.info(f"{self.name} - resetting device") + cmd = "*RST\n" + self.serial.write(cmd.encode()) + + def read(self): + """Read out all channels. + + Returns: + str: measurement data + """ + cmds = [ + "TRAC:CLE\n", + "TRAC:POIN 100\n", + f"ROUT:SCAN:CRE {self.reading_str}\n", + "ROUT:SCAN:COUN:SCAN 1\n", + "INIT\n", + "*WAI\n", + f'TRAC:DATA? 1,{self.nb_reading_values},"defbuffer1", CHAN, READ\n', + ] + for cmd in cmds: + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + def sample(self): + """Read sampling form device and convert values to specified format. + + Returns: + dict: {sensor name: measurement value} + """ + data = self.read().split(",") # = ['channel', 'value', 'channel', 'value', ...] + + if len(data) != 2 * self.nb_reading_values: # there is an error in the sampling + logging.error( + f"Sampling of Daq6510 '{self.name}' failed. Expected {2* self.nb_reading_values} values but got {data}" + ) + return {v: np.nan for _, v in self.channel_id_names.items()} + sampling = {} + for i in range(int(len(data) / 2)): + channel = int(data[2 * i]) + sensor_name = self.channel_id_names[channel] + measurement_value = ( + float(data[2 * i + 1]) * self.conversion_factor[sensor_name] + ) + sampling.update({sensor_name: measurement_value}) + return sampling + + +class IfmFlowmeter: + def __init__(self, config, name="IfmFlowmeter"): + """Prepare sampling. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): device name. + """ + logger.info(f"Initializing IfmFlowmeter device '{name}'") + self.config = config + self.name = name + self.ip = config["IP"] + self.ports = config["ports"] + self.meas_data = {"Temperature": {}, "Flow": {}} + for port_id in self.ports: + name = self.ports[port_id]["name"] + self.meas_data["Temperature"].update({name: []}) + self.meas_data["Flow"].update({name: []}) + self.last_sampling = {"Temperature": {}, "Flow": {}} + if "flow-balance" in config: + self.inflow_sensors = config["flow-balance"]["inflow"] + self.outflow_sensors = config["flow-balance"]["outflow"] + self.tolerance = config["flow-balance"]["tolerance"] + for sensor in self.inflow_sensors + self.outflow_sensors: + self.last_sampling["Flow"].update({sensor: 0}) + + def sample(self): + """Read sampling form device and convert values to readable format. + + Returns: + dict: {sensor name: measurement value} + """ + sampling = {"Temperature": {}, "Flow": {}} + for port in self.ports: + try: + name = self.ports[port]["name"] + sensor_type = self.ports[port]["type"] + r = requests.get( + f"http://{self.ip}/iolinkmaster/port[{port}]/iolinkdevice/pdin/getdata" + ) + data = r.json() + data_hex = data["data"]["value"] + l = len(data_hex) + if sensor_type == "SM-8020": + data_hex_t = data_hex[l - 8 : l - 4] + data_hex_f = data_hex[l - 16 : l - 12] + data_dec_t = 0.01 * int(data_hex_t, 16) + data_dec_f = 0.0166667 * int(data_hex_f, 16) + elif sensor_type == "SV-4200": + data_hex_t = data_hex[l - 4 :] + data_hex_f = data_hex[l - 8 : l - 4] + data_dec_t = 0.1 * int(data_hex_t, 16) / 4 + data_dec_f = 0.1 * int(data_hex_f, 16) + elif sensor_type == "SBG-233": + data_hex_t = data_hex[l - 4 :] + data_hex_f = data_hex[l - 8 : l - 4] + data_dec_t = 1.0 * int(data_hex_t, 16) / 4 + data_dec_f = 0.1 * int(data_hex_f, 16) + except Exception as e: + logger.exception(f"Could not sample IfmFlowmeter port '{name}'.") + data_dec_t = np.nan + data_dec_f = np.nan + sampling["Temperature"].update({name: data_dec_t}) + sampling["Flow"].update({name: data_dec_f}) + self.last_sampling = deepcopy(sampling) + return sampling + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to file. + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (dict): sampling data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel}," + for sensor in self.meas_data["Flow"]: + self.meas_data["Flow"][sensor].append(sampling["Flow"][sensor]) + line += f"{sampling['Flow'][sensor]}," + for sensor in self.meas_data["Temperature"]: + self.meas_data["Temperature"][sensor].append( + sampling["Temperature"][sensor] + ) + line += f"{sampling['Temperature'][sensor]}," + line += "\n" + with open(self.filename, "a", encoding="utf-8") as f: + f.write(line) + + def init_output(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + units = "# datetime,s," + header = "time_abs,time_rel," + for sensor in self.meas_data["Flow"]: + header += f"{sensor}-flow," + units += "l/min," + for sensor in self.meas_data["Temperature"]: + header += f"{sensor}-temperature," + units += "DEG C," + units += "\n" + header += "\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + self.write_nomad_files(directory) + + def write_nomad_files(self, directory="./"): + """Write .archive.yaml file based on device configuration. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + with open("./multilog/nomad/archive_template.yml") as f: + nomad_template = yaml.safe_load(f) + definitions = nomad_template.pop("definitions") + data = nomad_template.pop("data") + sensor_schema_template = nomad_template.pop("sensor_schema_template") + data.update( + { + "data_file": self.filename.split("/")[-1], + } + ) + for port in self.ports: + sensor_name = self.ports[port]["name"] + sensor_type = self.ports[port]["type"] + for property in ["flow", "temperature"]: + sensor_name_nomad = ( + f'{sensor_name.replace(" ", "_").replace("-", "_")}_{property}' + ) + data.update( + { + sensor_name_nomad: { + # "model": "your_field_here", + "name": sensor_name_nomad, + # "sensor_id": sensor_name.split(" ")[0], + "attached_to": sensor_name, + "measured_property": property, + "type": sensor_type, + # "notes": "TE_1_K air 155 mm over crucible", + # "unit": self.unit[sensor_name], # TODO + # "channel": channel, # TODO + "value_timestamp_rel": "#/data/value_timestamp_rel", + "value_timestamp_abs": "#/data/value_timestamp_abs", + } + } + ) + sensor_schema = deepcopy(sensor_schema_template) + sensor_schema["section"]["quantities"]["value_log"]["m_annotations"][ + "tabular" + ]["name"] = f"{sensor_name}-{property}" + definitions["sections"]["Sensors_list"]["sub_sections"].update( + {sensor_name_nomad: sensor_schema} + ) + definitions["sections"]["Sensors_list"]["m_annotations"]["plot"].append( + { + "label": f"{sensor_name_nomad} over time", + "x": "value_timestamp_rel", + "y": [f"{sensor_name_nomad}/value_log"], + } + ) + nomad_dict = { + "definitions": definitions, + "data": data, + } + with open(f"{directory}/{self.name}.archive.yaml", "w", encoding="utf-8") as f: + yaml.safe_dump(nomad_dict, f, sort_keys=False) + + def check_leakage(self): + """Evaluate flow balance as specified in config to check for + leakage. If leakage is detected a discord message is sent.""" + inflow = 0 + for sensor in self.inflow_sensors: + inflow += self.last_sampling["Flow"][sensor] + outflow = 0 + for sensor in self.outflow_sensors: + outflow += self.last_sampling["Flow"][sensor] + loss = inflow - outflow + if abs(loss) > self.tolerance: + logger.warning( + f"Detected possible cooling water leakage, difference of {loss} l/min" + ) + send_message( + f"There may be a cooling water leakage.\nThe difference between measured in- and outflow is {loss} l/min." + ) + + +class Eurotherm: + def __init__(self, config, name="Eurotherm"): + """Prepare sampling. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): device name. + """ + logger.info(f"Initializing Eurotherm device '{name}'") + self.config = config + self.name = name + try: + self.serial = Serial(**config["serial-interface"]) + except SerialException as e: + logger.exception(f"Connection to {self.name} not possible.") + self.serial = SerialMock() + + self.read_temperature = "\x040000PV\x05" + self.read_op = "\x040000OP\x05" + + self.meas_data = {"Temperature": [], "Operating point": []} + + def sample(self): + """Read sampling form device. + + Returns: + dict: {sensor name: measurement value} + """ + try: + self.serial.write(self.read_temperature.encode()) + temperature = float(self.serial.readline().decode()[3:-2]) + self.serial.write(self.read_op.encode()) + op = float(self.serial.readline().decode()[3:-2]) + except Exception as e: + logger.exception(f"Could not sample Eurotherm.") + temperature = np.nan + op = np.nan + return {"Temperature": temperature, "Operating point": op} + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to file. + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (dict): sampling data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + self.meas_data["Temperature"].append(sampling["Temperature"]) + self.meas_data["Operating point"].append(sampling["Operating point"]) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel},{sampling['Temperature']},{sampling['Operating point']},\n" + with open(self.filename, "a", encoding="utf-8") as f: + f.write(line) + + def init_output(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + units = "# datetime,s,DEG C,-,\n" + header = "time_abs,time_rel,Temperature,Operating point,\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + self.write_nomad_files(directory) + + def write_nomad_files(self, directory="./"): + """Write .archive.yaml file based on device configuration. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + with open("./multilog/nomad/archive_template.yml") as f: + nomad_template = yaml.safe_load(f) + definitions = nomad_template.pop("definitions") + data = nomad_template.pop("data") + sensor_schema_template = nomad_template.pop("sensor_schema_template") + data.update( + { + "data_file": self.filename.split("/")[-1], + } + ) + for sensor_name in self.meas_data: + sensor_name_nomad = sensor_name.replace(" ", "_").replace("-", "_") + data.update( + { + sensor_name_nomad: { + # "model": "your_field_here", + "name": sensor_name_nomad, + # "sensor_id": sensor_name.split(" ")[0], + # "attached_to": sensor_name, # TODO this information is important! + # "measured_property": , + # "type": sensor_type, + # "notes": "TE_1_K air 155 mm over crucible", + # "unit": self.unit[sensor_name], # TODO + "value_timestamp_rel": "#/data/value_timestamp_rel", + "value_timestamp_abs": "#/data/value_timestamp_abs", + } + } + ) + sensor_schema = deepcopy(sensor_schema_template) + sensor_schema["section"]["quantities"]["value_log"]["m_annotations"][ + "tabular" + ]["name"] = sensor_name + definitions["sections"]["Sensors_list"]["sub_sections"].update( + {sensor_name_nomad: sensor_schema} + ) + definitions["sections"]["Sensors_list"]["m_annotations"]["plot"].append( + { + "label": f"{sensor_name_nomad} over time", + "x": "value_timestamp_rel", + "y": [f"{sensor_name_nomad}/value_log"], + } + ) + nomad_dict = { + "definitions": definitions, + "data": data, + } + with open(f"{directory}/{self.name}.archive.yaml", "w", encoding="utf-8") as f: + yaml.safe_dump(nomad_dict, f, sort_keys=False) + + +class OptrisIP640: + """Optris Ip640 IR Camera.""" + + def __init__(self, config, name="OptrisIP640", xml_dir="./"): + """Initialize communication and configure device. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): Device name. + xml_dir(str, optional): Directory for xml-file with device + configuration. Defaults to "./". + """ + logger.info(f"Initializing OptrisIP640 device '{name}'") + self.name = name + self.emissivity = config["emissivity"] + logger.info(f"{self.name} - emissivity {self.emissivity}") + self.transmissivity = config["transmissivity"] + logger.info(f"{self.name} - transmissivity {self.transmissivity}") + self.t_ambient = config["T-ambient"] + logger.info(f"{self.name} - T-ambient {self.t_ambient}") + xml = f""" + +{config['serial-number']} +0 +/usr/share/libirimager +/usr/share/libirimager/cali +/root/.irimager/Cali + +33 + + {config['measurement-range'][0]} + {config['measurement-range'][1]} + + +{config['framerate']} +0 + + 1 + 15.0 + 0.0 + +0 +40.0 +50.0 +{config['extended-T-range']} +5 +0 + +""" + self.xml_file = f"{xml_dir}/{config['serial-number']}.xml" + with open(self.xml_file, "w", encoding="utf-8") as f: + f.write(xml) + try: + optris.usb_init( + self.xml_file + ) # This often fails on the first attempt, therefore just repeat in case of an error. + except Exception as e: + print( + f"Couldn't setup OptrisIP64.\n{traceback.format_exc()}\nTrying again..." + ) + logging.error(traceback.format_exc()) + optris.usb_init(self.xml_file) + optris.set_radiation_parameters( + self.emissivity, self.transmissivity, self.t_ambient + ) + self.w, self.h = optris.get_thermal_image_size() + self.meas_data = [] + self.image_counter = 1 + + def sample(self): + """Read image form device. + + Returns: + numpy.array: IR image (2D temperature filed) + """ + raw_image = optris.get_thermal_image(self.w, self.h) + thermal_image = (raw_image - 1000.0) / 10.0 # convert to temperature + return thermal_image + + @staticmethod + def plot_to_file(sampling, filename): + """Create a plot of the temperature distribution. This function + has to be called from a subprocess because matplotlib is not + threadsave. + + Args: + sampling (numpy array): IR image as returned from sample() + filename (str): filepath of plot + """ + fig, ax = plt.subplots() + ax.axis("off") + line = ax.imshow(sampling, cmap="turbo", aspect="equal") + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.05) + fig.colorbar(line, cax=cax) + fig.tight_layout() + fig.savefig(filename) + plt.close(fig) + + def init_output(self, directory="./"): + """Initialize the output subdirectory and csv file.. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.directory = f"{directory}/{self.name}" + os.makedirs(self.directory) + with open(f"{self.directory}/_images.csv", "w", encoding="utf-8") as f: + f.write("# datetime,s,filename,\n") + f.write("time_abs,time_rel,img-name,\n") + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to files: + - numpy array with temperature distribution + - png file with 2D IR image + - csv with metadata + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (numpy.array): sampling data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + self.meas_data = sampling + img_name = f"img_{self.image_counter:06}" + np.savetxt(f"{self.directory}/{img_name}.csv", sampling, "%.2f") + # plot in separate process because matplotlib is not threadsave + multiprocessing.Process( + target=self.plot_to_file, + args=(sampling, f"{self.directory}/{img_name}.png"), + ).start() + # self.plot_to_file(sampling, f"{self.directory}/{img_name}.png") + with open(f"{self.directory}/_images.csv", "a", encoding="utf-8") as f: + f.write( + f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel},{img_name},\n" + ) + self.image_counter += 1 + + def __del__(self): + """Terminate IR camera communictation and remove xml.""" + optris.terminate() + os.remove(self.xml_file) + + +class PyrometerLumasense: + """Lumasense pyrometer, e.g. IGA-6-23 or IGAR-6-adv.""" + + def __init__(self, config, name="PyrometerLumasense"): + """Setup serial interface, configure device. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): Device name. + """ + logger.info(f"Initializing PyrometerLumasense device '{name}'") + self.device_id = config["device-id"] + self.name = name + try: + self.serial = Serial(**config["serial-interface"]) + except SerialException as e: + logger.exception(f"Connection to {self.name} not possible.") + self.serial = SerialMock() + self.set_emissivity(config["emissivity"]) + self.set_transmissivity(config["transmissivity"]) + self.t90_dict = config["t90-dict"] + self.set_t90(config["t90"]) + self.meas_data = [] + + def _get_ok(self): + """Check if command was accepted.""" + assert self.serial.readline().decode().strip() == "ok" + + def _get_float(self): + """Read floatingpoint value.""" + string_val = self.serial.readline().decode().strip() + return float(f"{string_val[:-1]}.{string_val[-1:]}") + + @property + def focus(self): + """Get focuspoint.""" + cmd = f"{self.device_id}df\r" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + @property + def intrument_id(self): + """Get the instrument id.""" + cmd = f"{self.device_id}na\r" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + @property + def emissivity(self): + """Read the current emissivity.""" + cmd = f"{self.device_id}em\r" + self.serial.write(cmd.encode()) + return self._get_float() + + @property + def transmissivity(self): + """Read the current transmissivity.""" + cmd = f"{self.device_id}et\r" + self.serial.write(cmd.encode()) + return self._get_float() + + @property + def t90(self): + """Reat the current t90 value.""" + cmd = f"{self.device_id}ez\r" + self.serial.write(cmd.encode()) + idx = int(self.serial.readline().decode().strip()) + t90_dict_inverted = {v: k for k, v in self.t90_dict.items()} + return t90_dict_inverted[idx] + + def set_emissivity(self, emissivity): + """Set emissivity and check if it was accepted.""" + logger.info(f"{self.name} - setting emissivity {emissivity}") + cmd = f"{self.device_id}em{emissivity*100:05.1f}\r".replace(".", "") + self.serial.write(cmd.encode()) + self._get_ok() + assert self.emissivity == emissivity * 100 + + def set_transmissivity(self, transmissivity): + """Set transmissivity and check if it was accepted.""" + logger.info(f"{self.name} - setting transmissivity {transmissivity}") + cmd = f"{self.device_id}et{transmissivity*100:05.1f}\r".replace(".", "") + self.serial.write(cmd.encode()) + self._get_ok() + assert self.transmissivity == transmissivity * 100 + + def set_t90(self, t90): + """Set t90 and check if it was accepted.""" + logger.info(f"{self.name} - setting t90 {t90}") + cmd = f"{self.device_id}ez{self.t90_dict[t90]}\r" + self.serial.write(cmd.encode()) + self._get_ok() + assert self.t90 == t90 + + def sample(self): + """Read temperature form device. + + Returns: + float: temperature reading. + """ + try: + cmd = f"{self.device_id}ms\r" + self.serial.write(cmd.encode()) + val = self._get_float() + except Exception as e: + logger.exception(f"Could not sample PyrometerLumasense.") + val = np.nan + return val + + def init_output(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + units = "# datetime,s,DEG C,\n" + header = "time_abs,time_rel,Temperature,\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to file. + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (float): temperature, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + self.meas_data.append(sampling) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel},{sampling},\n" + with open(self.filename, "a", encoding="utf-8") as f: + f.write(line) + + +class PyrometerArrayLumasense: + """Lumasense pyrometer, e.g. Series 600.""" + + def __init__(self, config, name="PyrometerArrayLumasense"): + """Setup serial interface, configure device. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): Device name. + """ + logger.info(f"Initializing PyrometerArrayLumasense device '{name}'") + self.device_id = config["device-id"] + self.name = name + try: + self.serial = Serial(**config["serial-interface"]) + except SerialException as e: + logger.exception(f"Connection to {self.name} not possible.") + self.serial = SerialMock() + self.t90_dict = config["t90-dict"] + self.meas_data = {} + self.head_numbering = {} + self.sensors = [] + self.emissivities = {} + self.t90s = {} + for sensor in config["sensors"]: + self.sensors.append(sensor) + head_number = config["sensors"][sensor]["head-number"] + self.head_numbering.update({sensor: head_number}) + self.meas_data.update({sensor: []}) + self.emissivities.update({sensor: config["sensors"][sensor]["emissivity"]}) + self.t90s.update({sensor: config["sensors"][sensor]["t90"]}) + self.set_emissivity(head_number, config["sensors"][sensor]["emissivity"]) + self.set_emissivity(head_number, config["sensors"][sensor]["t90"]) + + def _get_ok(self): + """Check if command was accepted.""" + assert self.serial.readline().decode().strip() == "ok" + + def _get_float(self): + """Read floatingpoint value.""" + string_val = self.serial.readline().decode().strip() + return float(f"{string_val[:-1]}.{string_val[-1:]}") + + def get_heat_id(self, head_number): + """Get the id of a certain head.""" + cmd = f"{self.device_id}A{head_number}sn\r" + self.serial.write(cmd.encode()) + return self.serial.readline().decode().strip() + + def set_emissivity(self, head_number, emissivity): + """Set emissivity for a certain head.""" + logger.info( + f"{self.name} - setting emissivity {emissivity} for heat {head_number}" + ) + cmd = f"{self.device_id}A{head_number}em{emissivity*100:05.1f}\r".replace( + ".", "" + ) + self.serial.write(cmd.encode()) + self._get_ok() + + def set_t90(self, head_number, t90): + """Set t90 for a certain head.""" + logger.info(f"{self.name} - setting t90 {t90} for heat {head_number}") + cmd = f"{self.device_id}A{head_number}ez{self.t90_dict[t90]}\r" + self.serial.write(cmd.encode()) + self._get_ok() + + def read_sensor(self, head_number): + """Read temperature of a certain head.""" + cmd = f"{self.device_id}A{head_number}ms\r" + self.serial.write(cmd.encode()) + return self._get_float() + + def sample(self): + """Read temperature form all heads. + + Returns: + dict: {head name: temperature}. + """ + sampling = {} + for sensor in self.head_numbering: + try: + sampling.update({sensor: self.read_sensor(self.head_numbering[sensor])}) + except Exception as e: + logger.exception( + f"Could not sample PyrometerArrayLumasense heat '{sensor}'." + ) + sampling.update({sensor: np.nan}) + return sampling + + def init_otput(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + header = "time_abs,time_rel," + units = "# datetime,s," + for sensor in self.meas_data: + header += f"{sensor}," + units += "DEG C," + header += "\n" + units += "\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to file. + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (dict): measurement data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel}," + for sensor in sampling: + self.meas_data[sensor].append(sampling[sensor]) + line += f"{sampling[sensor]}," + line += "\n" + with open(self.filename, "a", encoding="utf-8") as f: + f.write(line) + + +class BaslerCamera: + """Basler optical camera.""" + + def __init__(self, config, name="BaslerCamera"): + """Setup pypylon, configure device. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): Device name. + """ + logger.info(f"Initializing BaslerCamera device '{name}'") + self.name = name + self._timeout = config["timeout"] + device_number = config["device-number"] + tl_factory = pylon.TlFactory.GetInstance() + self.device_name = tl_factory.EnumerateDevices()[ + device_number + ].GetFriendlyName() + self._device = pylon.InstantCamera() + self._device.Attach( + tl_factory.CreateDevice(tl_factory.EnumerateDevices()[device_number]) + ) + self._converter = pylon.ImageFormatConverter() + self._converter.OutputPixelFormat = pylon.PixelType_BGR8packed + self._converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned + self._name = tl_factory.EnumerateDevices()[device_number].GetFriendlyName() + self._model_number = self._name.split(" ")[1] + self._device_class = tl_factory.EnumerateDevices()[ + device_number + ].GetDeviceClass() + self._set_exposure_time(config["exposure-time"]) + self.set_frame_rate(config["frame-rate"]) + self._device.Open() + self._device.StartGrabbing(pylon.GrabStrategy_LatestImageOnly) + # self._device.StartGrabbing(pylon.GrabStrategy_UpcomingImage) + self.meas_data = [] + self.image_counter = 1 + + def _set_exposure_time(self, exposure_time): + """Set exposure time.""" + logger.info(f"{self.name} - setting exposure time {exposure_time}") + self._device.Open() + if self._device_class == "BaslerGigE": + assert ( + exposure_time <= self._device.ExposureTimeAbs.GetMax() + and exposure_time >= self._device.ExposureTimeAbs.GetMin() + ) + self._device.ExposureTimeAbs.SetValue(exposure_time) + elif self._device_class == "BaslerUsb": + assert ( + exposure_time <= self._device.ExposureTime.GetMax() + and exposure_time >= self._device.ExposureTime.GetMin() + ) + self._device.ExposureTime.SetValue(exposure_time) + else: + raise ValueError(f"Device class {self._device_class} is not supported!") + self._device.Close() + + def sample(self): + """Read latest image from device. + + Returns: + numpy.array: image. + """ + grab = self._device.RetrieveResult(self._timeout, pylon.TimeoutHandling_Return) + if grab and grab.GrabSucceeded(): + image = self._converter.Convert(grab).GetArray() + else: + raise RuntimeError("Image grabbing failed.") + grab.Release() + return image + + def init_output(self, directory="./"): + """Initialize the output subdirectory and csv file.. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.directory = f"{directory}/{self.name}" + os.makedirs(self.directory) + with open(f"{self.directory}/_images.csv", "w", encoding="utf-8") as f: + f.write("# datetime,s,filename,\n") + f.write("time_abs,time_rel,img-name,\n") + with open(f"{self.directory}/device.txt", "w", encoding="utf-8") as f: + f.write(self.device_name) + + def save_measurement(self, time_abs, time_rel, sampling): + """Write measurement data to files: + - jpg file with image + - csv with metadata + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (numpy.array): image as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + self.meas_data = sampling + img_name = f"img_{self.image_counter:06}.jpg" + Image.fromarray(sampling).convert("RGB").save(f"{self.directory}/{img_name}") + with open(f"{self.directory}/_images.csv", "a", encoding="utf-8") as f: + f.write( + f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel},{img_name},\n" + ) + self.image_counter += 1 + + def set_frame_rate(self, frame_rate): + """Set frame rate for continous sampling. The latest frame is + then grabbed by the sample function.""" + self._device.Open() + self._device.AcquisitionFrameRateEnable.SetValue(True) + if self._device_class == "BaslerGigE": + assert ( + frame_rate <= self._device.AcquisitionFrameRateAbs.GetMax() + and frame_rate >= self._device.AcquisitionFrameRateAbs.GetMin() + ) + self._device.AcquisitionFrameRateAbs.SetValue(frame_rate) + elif self._device_class == "BaslerUsb": + assert ( + frame_rate <= self._device.AcquisitionFrameRate.GetMax() + and frame_rate >= self._device.AcquisitionFrameRate.GetMin() + ) + self._device.AcquisitionFrameRate.SetValue(frame_rate) + else: + raise ValueError(f"Device class {self._device_class} is not supported!") + self._device.Close() + + def __del__(self): + """Stopp sampling, reset device.""" + self._device.StopGrabbing() + self._device.Close() + + +class ProcessConditionLogger: + """Virtual device for logging of process contidions. Instead of + sampling a physical devices the sampling are read from the user + input fields in the GUI.""" + + def __init__(self, config, name="ProcessConditionLogger"): + """Prepare sampling. + + Args: + config (dict): device configuration (as defined in + config.yml in the devices-section). + name (str, optional): device name. + """ + self.config = config + self.name = name + self.meas_data = {} + self.condition_units = {} + for condition in config: + default = "" + if "default" in config[condition]: + default = config[condition]["default"] + self.meas_data.update({condition: default}) + if "unit" in config[condition]: + self.condition_units.update({condition: config[condition]["unit"]}) + else: + self.condition_units.update({condition: ""}) + self.last_meas_data = deepcopy(self.meas_data) + + def init_output(self, directory="./"): + """Initialize the csv output file. + + Args: + directory (str, optional): Output directory. Defaults to "./". + """ + self.filename = f"{directory}/{self.name}.csv" + header = "time_abs,time_rel," + units = "# datetime,s," + for condition in self.meas_data: + header += f"{condition}," + units += f"{self.condition_units[condition]}," + header += "\n" + units += "\n" + with open(self.filename, "w", encoding="utf-8") as f: + f.write(units) + f.write(header) + self.protocol_filename = f"{directory}/protocol_{self.name}.md" + with open(self.protocol_filename, "w", encoding="utf-8") as f: + f.write("# Multilog protocol\n\n") + multilog_version = ( + subprocess.check_output( + ["git", "describe", "--tags", "--dirty", "--always"] + ) + .strip() + .decode("utf-8") + ) + f.write(f"This is multilog version {multilog_version}.\n") + f.write( + f"Logging stated at {datetime.datetime.now():%d.%m.%Y, %H:%M:%S}.\n\n" + ) + f.write("## Initial process conditions\n\n") + for condition in self.meas_data: + f.write( + f"- {condition}: {self.meas_data[condition]} {self.condition_units[condition]}\n" + ) + f.write("\n## Process condition log\n\n") + + def sample(self): + """This function just exists to fit into the sampling structure + of multilog. Data is directly updated upon changes in the GUI. + + Returns: + dict: {process condition: user input} + """ + # meas_data is updated by the widget if changes are made. No real sampling required. + return self.meas_data + + def save_measurement(self, time_abs, time_rel, sampling): + """Write sampling data to file. This function creates to files: + - A csv file with all values for each timestep (follwoing the + standard sampling procedure) + - A readme.md file with the initial values and timestamp + value + for each change in the process conditons (similar as people + write it to their labbook) + + Args: + time_abs (datetime): measurement timestamp. + time_rel (float): relative time of measurement. + sampling (dict): sampling data, as returned from sample() + """ + timediff = ( + datetime.datetime.now(datetime.timezone.utc).astimezone() - time_abs + ).total_seconds() + if timediff > 1: + logger.warning( + f"{self.name} save_measurement: time difference between event and saving of {timediff} seconds for samplint timestep {time_abs.isoformat(timespec='milliseconds').replace('T', ' ')} - {time_rel}" + ) + line = f"{time_abs.isoformat(timespec='milliseconds').replace('T', ' ')},{time_rel}," + for condition in sampling: + line += f"{sampling[condition]}," + line += "\n" + with open(self.filename, "a", encoding="utf-8") as f: + f.write(line) + + if self.meas_data != self.last_meas_data and time_rel > 1: + with open(self.protocol_filename, "a", encoding="utf-8") as f: + for condition in self.meas_data: + if self.meas_data[condition] != self.last_meas_data[condition]: + f.write( + f"- {time_abs.strftime('%d.%m.%Y, %H:%M:%S')}, {time_rel:.1f} s, {condition}: {self.meas_data[condition]} {self.condition_units[condition]}\n" + ) + self.last_meas_data = deepcopy(self.meas_data) diff --git a/multilog/discord_bot.py b/multilog/discord_bot.py new file mode 100644 index 0000000..56bd40e --- /dev/null +++ b/multilog/discord_bot.py @@ -0,0 +1,38 @@ +"""Bot for sending discord messages on a pre-configured computer. +Refer to the discord docs for additional information.""" +import asyncio +import os +from os.path import expanduser +import logging + +logger = logging.getLogger(__name__) +try: + from dotenv import load_dotenv +except Exception as e: + logger.warning("Could not import dotenv.", exc_info=True) +try: + from discord.ext import commands +except Exception as e: + logger.warning("Could not import discord.", exc_info=True) + + +logger = logging.getLogger(__name__) + + +def send_message(msg): + logger.info(f"Sending discord message '{msg}'") + asyncio.set_event_loop(asyncio.new_event_loop()) + load_dotenv(expanduser("~") + "/discord.env") + DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") + DISCORD_CHANNEL = int(os.getenv("DISCORD_CHANNEL")) + + bot = commands.Bot(command_prefix="cmd!", description="I am NemoOneBot") + + @bot.event + async def on_ready(): + channel = bot.get_channel(DISCORD_CHANNEL) + await channel.send(msg) + await asyncio.sleep(0.5) + await bot.close() + + bot.run(DISCORD_TOKEN) # blocking call! diff --git a/Exit-icon.png b/multilog/icons/Exit-icon.png similarity index 100% rename from Exit-icon.png rename to multilog/icons/Exit-icon.png diff --git a/Pause-icon.png b/multilog/icons/Pause-icon.png similarity index 100% rename from Pause-icon.png rename to multilog/icons/Pause-icon.png diff --git a/Start-icon.png b/multilog/icons/Start-icon.png similarity index 100% rename from Start-icon.png rename to multilog/icons/Start-icon.png diff --git a/multilog/icons/nemocrys.png b/multilog/icons/nemocrys.png new file mode 100644 index 0000000..3dc3474 Binary files /dev/null and b/multilog/icons/nemocrys.png differ diff --git a/multilog/main.py b/multilog/main.py new file mode 100644 index 0000000..8a85924 --- /dev/null +++ b/multilog/main.py @@ -0,0 +1,361 @@ +import shutil +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QTimer, QThread, QObject, pyqtSignal +import numpy as np +import datetime +import yaml +import sys +import os +import subprocess +import platform +import logging +import time + + +logger = logging.getLogger(__name__) + + +# This metaclass is required because the pyqtSignal 'signal' must be a class varaible +# see https://stackoverflow.com/questions/50294652/is-it-possible-to-create-pyqtsignals-on-instances-at-runtime-without-using-class +class SignalMetaclass(type(QObject)): + """Metaclass used to create new signals on the fly.""" + + def __new__(cls, name, bases, dct): + """Create new class including a pyqtSignal.""" + dct["signal"] = pyqtSignal(dict) + return super().__new__(cls, name, bases, dct) + + +class Sampler(QObject, metaclass=SignalMetaclass): + """This class is used to sample the devices from separate threads.""" + + def __init__(self, devices): + """Create sampler object + + Args: + devices (dict): devices to be sampled. + """ + super().__init__() + self.devices = devices + + def update(self): + """Sampling during initialization. Data is not saved.""" + sampling = {} + for device in self.devices: + try: + logger.debug(f"Sampler: updating {device}") + sampling.update({device: self.devices[device].sample()}) + logger.debug(f"Sampler: updated {device}") + except Exception as e: + logger.exception(f"Error in sampling of {device}") + self.signal.emit(sampling) # update graphics + + def sample(self, time): + """Sampling during recording. Data is visualized and saved. + + Args: + time (datetime): Global timestamp of sampling step. + """ + time_abs = time["time_abs"] + time_rel = time["time_rel"] + meas_data = {} + for device in self.devices: + try: + logger.debug( + f"Sampler: sampling {device}, timestep {time_abs.isoformat(timespec='milliseconds')} - {time_rel}" + ) + sampling = self.devices[device].sample() + self.devices[device].save_measurement(time_abs, time_rel, sampling) + meas_data.update({device: self.devices[device].meas_data}) + logger.debug(f"Sampler: sampled {device}") + except Exception as e: + logger.exception(f"Error in sampling of {device}") + self.signal.emit(meas_data) # update graphics + + +class Controller(QObject): + """Main class controlling multilog's sampling and data visualization.""" + + # signals to communicate with threads + signal_update_main = pyqtSignal() # sample and update view + signal_sample_main = pyqtSignal(dict) # sample, update view and save + signal_update_camera = pyqtSignal() # sample and update view + signal_sample_camera = pyqtSignal(dict) # sample, update view and save + + def __init__(self) -> None: + """Initialize and run multilog.""" + super().__init__() + + # load configuration, setup logging + with open("./config.yml", encoding="utf-8") as f: + self.config = yaml.safe_load(f) + logging.basicConfig(**self.config["logging"]) + logging.info("initializing multilog") + logging.info(f"configuration: {self.config}") + + # do that after logging has been configured to log possible errors + from .devices import ( + BaslerCamera, + Daq6510, + IfmFlowmeter, + Eurotherm, + OptrisIP640, + ProcessConditionLogger, + PyrometerArrayLumasense, + PyrometerLumasense, + ) + from .view import ( + BaslerCameraWidget, + IfmFlowmeterWidget, + MainWindow, + Daq6510Widget, + EurothermWidget, + OptrisIP640Widget, + ProcessConditionLoggerWidget, + PyrometerLumasenseWidget, + PyrometerArrayLumasenseWidget, + ) + + self.sampling_started = False # this will to be true once "start" was clicked + + # setup timers that emit the signals for sampling + # main sampling loop + self.timer_measurement_main = QTimer() + self.timer_measurement_main.setInterval(self.config["settings"]["dt-main"]) + self.timer_measurement_main.timeout.connect(self.sample_main) + # camera sampling loop + self.timer_measurement_camera = QTimer() + self.timer_measurement_camera.setInterval(self.config["settings"]["dt-camera"]) + self.timer_measurement_camera.timeout.connect(self.sample_camera) + # main sampling loop after startup (without saving data) + self.timer_update_main = QTimer() + self.timer_update_main.setInterval(self.config["settings"]["dt-init"]) + self.timer_update_main.timeout.connect(self.update_main) + # camera frame update loop + self.timer_update_camera = QTimer() + self.timer_update_camera.setInterval( + self.config["settings"]["dt-camera-update"] + ) + self.timer_update_camera.timeout.connect(self.update_camera) + + # time information is stored globally + # TODO this may be the reason for the race condition in IFM-flowmeter sampling + self.start_time = None + self.abs_time = [] + self.rel_time = [] + + # setup main window + app = QApplication(sys.argv) + self.main_window = MainWindow(self.start, self.exit) + if app.desktop().screenGeometry().width() == 1280: + self.main_window.resize(1180, 900) + self.main_window.move(10, 10) + + # setup devices & tabs + self.devices = {} + self.tabs = {} + self.cameras = [] + for device_name in self.config["devices"]: + if "DAQ-6510" in device_name: + device = Daq6510(self.config["devices"][device_name], device_name) + widget = Daq6510Widget(device) + elif "IFM-flowmeter" in device_name: + device = IfmFlowmeter(self.config["devices"][device_name], device_name) + widget = IfmFlowmeterWidget(device) + elif "Eurotherm" in device_name: + device = Eurotherm(self.config["devices"][device_name], device_name) + widget = EurothermWidget(device) + elif "Optris-IP-640" in device_name: + device = OptrisIP640(self.config["devices"][device_name], device_name) + widget = OptrisIP640Widget(device) + self.cameras.append(device_name) + elif "IGA-6-23" in device_name or "IGAR-6-adv" in device_name: + device = PyrometerLumasense( + self.config["devices"][device_name], device_name + ) + widget = PyrometerLumasenseWidget(device) + elif "Series-600" in device_name: + logger.warning("Series-600 devices haven't been tested yet.") + device = PyrometerArrayLumasense( + self.config["devices"][device_name], device_name + ) + widget = PyrometerArrayLumasenseWidget(device) + elif "Basler" in device_name: + device = BaslerCamera(self.config["devices"][device_name], device_name) + widget = BaslerCameraWidget(device) + self.cameras.append(device_name) + elif "Process-Condition-Logger" in device_name: + device = ProcessConditionLogger( + self.config["devices"][device_name], device_name + ) + widget = ProcessConditionLoggerWidget(device) + ####################### + # add new devices here! + ####################### + else: + raise ValueError(f"unknown device {device_name} in config file.") + + self.devices.update({device_name: device}) + self.main_window.add_tab(widget, device_name) + self.tabs.update({device_name: widget}) + + # setup threads + self.samplers = [] + self.threads = [] + for device in self.devices: + thread = QThread() + sampler = Sampler({device: self.devices[device]}) + sampler.moveToThread(thread) + sampler.signal.connect(self.update_view) + if device in self.cameras: + self.signal_update_camera.connect(sampler.update) + self.signal_sample_camera.connect(sampler.sample) + else: + self.signal_update_main.connect(sampler.update) + self.signal_sample_main.connect(sampler.sample) + self.samplers.append(sampler) + self.threads.append(thread) + + # run + for thread in self.threads: + thread.start() + self.timer_update_main.start() + self.timer_update_camera.start() + self.main_window.show() + sys.exit(app.exec()) + + def update_view(self, device_sampling): + """Update the view for selected devices. This is called by the + Sampler class's update function (using a signal). + + Args: + device_sampling (dict): {device-name: sampling} + """ + try: + for device in device_sampling: + logger.debug(f"updating view {device}") + if not self.sampling_started: + self.tabs[device].set_initialization_data(device_sampling[device]) + else: + self.tabs[device].set_measurement_data( + self.rel_time, device_sampling[device] + ) + logger.debug(f"updated view {device}") + except Exception as e: + logger.exception(f"Error in updating view of {device}") + + def start(self): + """This is executed when the start button is clicked.""" + logger.info("Stop updating.") + self.timer_update_main.stop() + time.sleep(1) # to finish running update jobs (running in separate threads) + logger.info("Start sampling.") + self.init_output_files() + self.start_time = datetime.datetime.now(datetime.timezone.utc).astimezone() + self.main_window.set_start_time(self.start_time.strftime("%d.%m.%Y, %H:%M:%S")) + self.sampling_started = True + self.timer_measurement_main.start() + self.sample_main() + self.timer_measurement_camera.start() + self.sample_camera() + + def exit(self): + """This is executed when the exit button is clicked.""" + logger.info("Stopping sampling") + self.timer_update_camera.stop() + self.timer_measurement_main.stop() + self.timer_measurement_camera.stop() + time.sleep(1) # to finish last sampling jobs (running in separate threads) + for thread in self.threads: + thread.quit() + logger.info("Stopped sampling") + exit() + + def init_output_files(self): + """Create directory for sampling and initialize output files.""" + logger.info("Setting up output files.") + date = datetime.datetime.now().strftime("%Y-%m-%d") + for i in range(100): + if i == 99: + raise ValueError("Too high directory count.") + self.directory = f"./measdata_{date}_#{i+1:02}" + if not os.path.exists(self.directory): + os.makedirs(self.directory) + break + for device in self.devices: + self.devices[device].init_output(self.directory) + self.write_metadata() + shutil.copy( + "./multilog/nomad/base_classes.schema.archive.yaml", + f"{self.directory}/base_classes.schema.archive.yaml", + ) + self.main_window.set_output_directory(self.directory) + + def write_metadata(self): + """Write a csv file with information about multilog version, + python version and operating system. + """ + multilog_version = ( + subprocess.check_output( + ["git", "describe", "--tags", "--dirty", "--always"] + ) + .strip() + .decode("utf-8") + ) + metadata = f"multilog version,python version,system information,\n" + metadata += f"{multilog_version},{platform.python_version()},{str(platform.uname()).replace(',',';')},\n" + with open(f"{self.directory}/config.yml", "w", encoding="utf-8") as f: + yaml.dump(self.config, f) + with open(f"{self.directory}/metadata.csv", "w", encoding="utf-8") as f: + f.write(metadata) + + def update_main(self): + """Function that triggers sampling after startup (without saving). + This function is called by a timer and leads to a call of the + update function of the Sampler objects (running in their + respective threads).""" + logger.info("update main") + self.main_window.set_current_time(datetime.datetime.now().strftime("%H:%M:%S")) + self.signal_update_main.emit() + if "IFM-flowmeter" in self.devices: + flowmeter = self.devices["IFM-flowmeter"] + flowmeter.check_leakage() + + def update_camera(self): + """Function that triggers graphics update for cameras (without saving). + This function is called by a timer and leads to a call of the + update function of the Sampler objects (running in their + respective threads).""" + logger.info("update camera") + self.signal_update_camera.emit() + + def sample_main(self): + """Function that triggers sampling & saving of data. + This function is called by a timer and leads to a call of the + sample function of the Sampler objects (running in their + respective threads).""" + logger.info("sample main") + time_abs = datetime.datetime.now(datetime.timezone.utc).astimezone() + time_rel = round((time_abs - self.start_time).total_seconds(), 3) + self.abs_time.append(time_abs) + self.rel_time.append(time_rel) + self.main_window.set_current_time(f"{time_abs:%H:%M:%S}") + self.signal_sample_main.emit({"time_abs": time_abs, "time_rel": time_rel}) + if "IFM-flowmeter" in self.devices: + flowmeter = self.devices["IFM-flowmeter"] + flowmeter.check_leakage() + + def sample_camera(self): + """Function that triggers sampling & saving of data for cameras. + This function is called by a timer and leads to a call of the + sample function of the Sampler objects (running in their + respective threads).""" + logger.info("sample camera") + time_abs = datetime.datetime.now(datetime.timezone.utc).astimezone() + time_rel = round((time_abs - self.start_time).total_seconds(), 3) + self.signal_sample_camera.emit({"time_abs": time_abs, "time_rel": time_rel}) + + +def main(): + """Function to execute multilog.""" + ctrl = Controller() diff --git a/multilog/nomad/archive_template.yml b/multilog/nomad/archive_template.yml new file mode 100644 index 0000000..0a74ff2 --- /dev/null +++ b/multilog/nomad/archive_template.yml @@ -0,0 +1,97 @@ +definitions: + name: 'Sensors' + sections: + Sensors_list: + base_sections: + - nomad.parsing.tabular.TableData + - nomad.datamodel.data.EntryData + m_annotations: + plot: [] + # - label: My test plot label 1 + # x: value_timestamp_rel + # y: + # - temperature_sensor_1/value_log + # - pyrosensor_2/value_log + quantities: + data_file: + type: str + description: | + A reference to an uploaded .csv + m_annotations: + tabular_parser: + sep: ',' + comment: '#' + browser: + adaptor: RawFileAdaptor + eln: + component: FileEditQuantity + value_timestamp_rel: + type: np.float64 + shape: ['*'] + m_annotations: + tabular: + name: time_rel + description: Relative time + value_timestamp_abs: + type: Datetime + shape: ['*'] + m_annotations: + tabular: + name: time_abs + description: | + Timestamp for when the values provided in the value field were registered. + Individual readings can be stored with their timestamps under value_log. + This is to timestamp the nominal setpoint or + average reading values listed above in the value field. + sub_sections: + {} +data: + m_def: Sensors_list + data_file: test.csv + +sensor_schema_template: + section: + m_annotations: + plot: + - label: My test plot label 1 + x: value_timestamp_rel + y: value_log + base_section: ../upload/raw/base_classes.schema.archive.yaml#Sample + quantities: + value_log: # The one we actually measurement + type: np.float64 # TODO try if that can be removed, it's already present in parent class + shape: ['*'] + m_annotations: + tabular: + name: TE_1_K air 155 mm over crucible + description: Time history of sensor readings. May differ from setpoint + value_timestamp_rel: + type: + type_kind: quantity_reference + type_data: '#/definitions/section_definitions/0/quantities/1' + value_timestamp_abs: + type: + type_kind: quantity_reference + type_data: '#/definitions/section_definitions/0/quantities/2' + # emission: + # type: int + # m_annotations: + # eln: + # component: NumberEditQuantity + # description: "Emission percentage value set in pyrometer" + # transmission: + # type: int + # m_annotations: + # eln: + # component: NumberEditQuantity + # description: "Transmission percentage value set in pyrometer" + # t90: + # type: np.float64 + # description: "FILL THE DESCRIPTION" + # unit: second + # m_annotations: + # eln: + # component: NumberEditQuantity + # defaultDisplayUnit: second + # TODO keep listing all the quantities here + diff --git a/multilog/nomad/base_classes.schema.archive.yaml b/multilog/nomad/base_classes.schema.archive.yaml new file mode 100644 index 0000000..6e78394 --- /dev/null +++ b/multilog/nomad/base_classes.schema.archive.yaml @@ -0,0 +1,730 @@ +definitions: + name: 'Multiple Base Class' + sections: # Schemes consist of section definitions + Entity: + m_annotations: + eln: + dict() + base_sections: + - nomad.datamodel.data.EntryData + sub_sections: + users: + section: + quantities: + responsible_person: + type: Author + shape: ['*'] + m_annotations: + eln: + component: AuthorEditQuantity + history: + section: + m_annotations: + eln: + quantities: + activities: + type: Activity + shape: ['*'] + m_annotations: + eln: + component: ReferenceEditQuantity + Experiment: + base_section: '#/Entity' + quantities: + experiment_goal: + type: str + description: indicate here the goal aimed with this experiment + m_annotations: + eln: + component: StringEditQuantity + Material: + base_section: '#/Entity' + quantities: + test: + type: str + m_annotations: + eln: + component: StringEditQuantity + Substrate: + base_section: '#/Material' + sub_sections: + SampleID: + section: + base_sections: + - 'nomad.datamodel.metainfo.eln.SampleID' + - 'nomad.datamodel.data.EntryData' + m_annotations: + template: + eln: + # hide: ['children', 'parents'] + # hide: ['children', 'parents', institute] + hide: [] + composition: + repeats: true + #m_annotations: + # eln: + section: '#/EntityAndAmount' + quantities: + comment: + type: str + m_annotations: + eln: + component: StringEditQuantity + supplier: + type: str + description: Sample preparation including orientating, polishing, cutting done by this company + m_annotations: + eln: + component: StringEditQuantity + orientation: + type: str + description: crystallographic orientation of the substrate in [hkl] + m_annotations: + eln: + component: StringEditQuantity + off_cut: + type: np.float64 + unit: degrees + description: Off-cut angle to the substrates surface + m_annotations: + eln: + component: NumberEditQuantity + doping_level: + type: np.float64 + #unit: wt % + description: Chemical doping level of electrically conductive substrates + m_annotations: + eln: + component: NumberEditQuantity + doping_species: + type: str + description: Doping species to obtain electrical conductivity in the substrates + m_annotations: + eln: + component: StringEditQuantity + charge: + type: str + description: Substrate charge ID given by fabrication company. Detailed information can be obtained from the company by requesting this charge ID + m_annotations: + eln: + component: StringEditQuantity + size: + type: str + description: Substrate dimensions + m_annotations: + eln: + component: StringEditQuantity + prepared: + type: str + description: Is the sample annealed, cleaned and etched for smooth stepped surface? + m_annotations: + eln: + component: StringEditQuantity + recycled: + type: str + description: Was the substrate deposited already and is recycled by polishing? + m_annotations: + eln: + component: StringEditQuantity + Sample: + base_section: '#/Material' + sub_sections: + SampleID: + section: + base_sections: + - 'nomad.datamodel.metainfo.eln.SampleID' + - 'nomad.datamodel.data.EntryData' + m_annotations: + template: + eln: + # hide: ['children', 'parents'] + # hide: ['children', 'parents', institute] + hide: [] + geometry: + section: + m_annotations: + eln: + dict() + sub_sections: + parallelepiped: + section: + quantities: + height: + type: np.float64 + unit: nanometer + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: nanometer + width: + type: np.float64 + unit: millimeter + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter + length: + type: np.float64 + unit: millimeter + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter + surface_area: + type: np.float64 + unit: millimeter ** 2 + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter ** 2 + volume: + type: np.float64 + unit: millimeter ** 3 + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter ** 3 + cylinder: + section: + quantities: + height: + type: np.float64 + unit: nanometer + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: nanometer + radius: + type: np.float64 + unit: millimeter + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter + lower_cap_radius: + type: np.float64 + unit: millimeter + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter + upper_cap_radius: + type: np.float64 + unit: millimeter + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter + cap_surface_area: + type: np.float64 + unit: millimeter ** 2 + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter ** 2 + lateral_surface_area: + type: np.float64 + unit: millimeter ** 2 + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter ** 2 + volume: + type: np.float64 + unit: millimeter ** 3 + description: docs + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: millimeter ** 3 + quantities: + # data_source: + # type: + # type_kind: Enum + # type_data: + # - experimental + # - simulation + # - declared_by_vendor + # - nominal + # m_annotations: + # eln: + # component: RadioEnumEditQuantity + # synthesis_method: + # type: + # type_kind: Enum + # type_data: + # - MOVPE + # - what not + # m_annotations: + # eln: + # component: RadioEnumEditQuantity + comment: + type: str + m_annotations: + eln: + component: StringEditQuantity + iupac_name: + type: str + m_annotations: + eln: + component: StringEditQuantity + empirical_formula: + type: str + m_annotations: + eln: + component: StringEditQuantity + state_or_phase: + type: + type_kind: Enum + type_data: + - cristalline solid + - microcristalline solid + - powder + m_annotations: + eln: + component: RadioEnumEditQuantity + preparation_date: + type: str + description: creation date + m_annotations: + eln: + component: StringEditQuantity + components: + type: '#/Material' + shape: ['*'] + m_annotations: + eln: + component: ReferenceEditQuantity + Activity: + base_section: "nomad.datamodel.data.EntryData" + quantities: + start_time: + description: | + The starting date and time of the activity. + type: Datetime + m_annotations: + eln: + component: DateTimeEditQuantity + end_time: + description: | + The ending date and time of the activity. + type: Datetime + m_annotations: + eln: + component: DateTimeEditQuantity + input_materials: + type: '#/Material' + shape: ['*'] + m_annotations: + eln: + component: ReferenceEditQuantity + output_materials: + type: '#/Material' + shape: ['*'] + m_annotations: + eln: + component: ReferenceEditQuantity + activity_category: + type: + type_kind: Enum + type_data: + - crystal growth synthesis + - sample preparation synthesis + - epitaxial growth synthesis + - sol-gel synthesis + - surface coating synthesis + - measurement experiment + description: | + A phenomenon by which change takes place in a system. + In physiological systems, a process may be + chemical, physical or both. + [IUPAC Gold Book](https://goldbook.iupac.org/terms/view/P04858) + m_annotations: + eln: + component: EnumEditQuantity + activity_method: + type: + type_kind: Enum + type_data: + - ALL THE TECHNIQUES HERE + - Melt Czochralski + description: | + a method is a series of steps for performing a function or accomplishing a result. + Or + any systematic way of obtaining information about a scientific nature or to obtain a desired material or product + m_annotations: + eln: + component: EnumEditQuantity + activity_identifier: + type: str + m_annotations: + # tabular: + # name: Overview/Experiment Identifier + eln: + component: StringEditQuantity + location: + type: str + m_annotations: + # tabular: + # name: Overview/Experiment Location + eln: + component: StringEditQuantity + sub_sections: + users: + section: + quantities: + responsible_person: + type: Author + shape: ['*'] + m_annotations: + eln: + component: AuthorEditQuantity + operator: + type: Author + shape: ['*'] + m_annotations: + eln: + component: AuthorEditQuantity + Measurement: + base_section: '#/Activity' + Procedure_step: + more: + label_quantity: 'step_name' + quantities: + step_name: + type: str + description: what this step consists of + m_annotations: + eln: + component: StringEditQuantity + # step_type: + # type: + # type_kind: Enum + # type_data: + # - Pre-process + # - Process + # - Post-process + # - Measurement + # - Storage + # m_annotations: + # eln: + # component: EnumEditQuantity + step_number: + type: int + description: sequential number of the step on going + m_annotations: + eln: + component: NumberEditQuantity + step_comment: + type: str + description: more verbose description of the step + m_annotations: + eln: + component: StringEditQuantity + step_duration: + type: np.float64 + unit: minute + description: Past time since process start + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: minute + elapsed_time: + type: np.float64 + unit: minute + description: Duration of each step + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: minute + Process: + base_section: '#/Activity' + quantities: + comment: + type: str + description: my descr + m_annotations: + eln: + component: StringEditQuantity + sub_sections: + procedure: + repeats: true + section: '#/Procedure_step' + Substance: + base_section: '#/Material' + more: + label_quantity: iupac_name + sub_sections: + SampleID: + section: + base_sections: + - 'nomad.datamodel.metainfo.eln.SampleID' + - 'nomad.datamodel.data.EntryData' + m_annotations: + template: + eln: + # hide: ['children', 'parents'] + # hide: ['children', 'parents', institute] + hide: [] + quantities: + comment: + type: str + m_annotations: + eln: + component: StringEditQuantity + empirical_formula: + type: str + description: chemical formula + m_annotations: + eln: + component: StringEditQuantity + iupac_name: + type: str + description: the IUPAC nomenclature of the chemical + m_annotations: + eln: + component: StringEditQuantity + state_or_phase: + type: str + description: Phase of the chemical in the bottle + m_annotations: + eln: + component: StringEditQuantity + supplier: + type: str + description: Fabricating company + m_annotations: + eln: + component: StringEditQuantity + purity: + type: + type_kind: Enum + type_data: + - Puratronic 99.995% + - Puratronic 99.999% + - REacton 99.995% + - REacton 99.999% + - ACS grade + - Reagent grade + - USP grade + - NF grade + - BP grade + - JP grade + - Laboratory grade + - Purified grade + - Technical grade + description: Purity of the Chemical. [Wikipedia](https://en.wikipedia.org/wiki/Chemical_purity) + m_annotations: + eln: + component: EnumEditQuantity + buying_date: + type: Datetime + description: Date of the Invoice Mail + m_annotations: + eln: + component: DateTimeEditQuantity + opening_date: + type: Datetime + description: Date of Opening the Chemical bottle in the Glove box + m_annotations: + eln: + component: DateTimeEditQuantity + batch_number: + type: str + description: batch number of chemical + m_annotations: + eln: + component: StringEditQuantity + cas_number: + type: str + description: CAS number + m_annotations: + eln: + component: StringEditQuantity + sku_number: + type: str + description: sku number + m_annotations: + eln: + component: StringEditQuantity + smiles: + type: str + description: smiles string indentifier + m_annotations: + eln: + component: StringEditQuantity + inchi: + type: str + description: inchi string indentifier + m_annotations: + eln: + component: StringEditQuantity + documentation: + type: str + description: pdf files containing certificate and other documentation + m_annotations: + browser: + adaptor: RawFileAdaptor # Allows to navigate to files in the data browser + eln: + component: FileEditQuantity + EntityAndAmount: + base_section: '#/Material' + quantities: + component: + type: Material + m_annotations: + eln: + component: ReferenceEditQuantity + mass: + type: np.float64 + unit: mg + description: | + Mass of the powder precursor weighted out in the glove box + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: mg + amount: + type: np.float64 + unit: mmol + description: | + Amount of substance of precursor powder weighted out + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: mmol + volume_solvent: + type: np.float64 + unit: ml + description: | + Volume of solvent used to solve the powder precursor + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: ml + mass_concentration: + type: np.float64 + unit: g/L + description: | + Mass concentration of the prepared precursor-solvent solution + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: g/L + molar_concentration: + type: np.float64 + unit: mmol/L + description: | + Amount of substance concentration of the prepared precursor-solvent solution + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: mmol/L + flow: + type: np.float64 + unit: mL/minute + description: | + Velocity of the precursor solution flow adjusted by peristaltic pumps + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: mL/minute + Sensor: + quantities: + model: + type: str + m_annotations: + eln: + component: StringEditQuantity + name: + type: str + m_annotations: + eln: + component: StringEditQuantity + description: "name for the sensor" + sensor_id: + type: str + m_annotations: + eln: + component: StringEditQuantity + description: "ID of the applied sensor" + attached_to: + type: str + m_annotations: + eln: + component: StringEditQuantity + description: your port or channel where sensor is attached + measured_property: + type: + type_kind: Enum + type_data: + - temperature + - pH + - magnetic_field + - electric_field + - conductivity + - resistance + - voltage + - pressure + - flow + - stress + - strain + - shear + - surface_pressure + description: "name for measured signal" + m_annotations: + eln: + component: EnumEditQuantity + type: + type: str + m_annotations: + eln: + component: StringEditQuantity + description: | + The type of hardware used for the measurement. + Examples (suggestions but not restrictions): + Temperature: J | K | T | E | R | S | Pt100 | Rh/Fe + pH: Hg/Hg2Cl2 | Ag/AgCl | ISFET + Ion selective electrode: specify species; e.g. Ca2+ + Magnetic field: Hall + Surface pressure: wilhelmy plate + notes: + type: str + m_annotations: + eln: + component: StringEditQuantity + description: "Notes or comments for the sensor" + value_set: + type: np.float64 + shape: ['*'] + description: | + For each point in the scan space, either the nominal + setpoint of an independently scanned controller + or a representative average value of a measurement sensor is registered. + value_log: + type: np.float64 + shape: ['*'] + description: Time history of sensor readings. May differ from setpoint + value_timestamp_rel: + type: np.float64 + shape: ['*'] + description: Relative time in measurement series. + value_timestamp_abs: + type: Datetime + shape: ['*'] + description: | + Timestamp for when the values provided in the value field were registered. + Individual readings can be stored with their timestamps under value_log. + This is to timestamp the nominal setpoint or + average reading values listed above in the value field. diff --git a/multilog/pyOptris/__init__.py b/multilog/pyOptris/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/multilog/pyOptris/direct_binding.py b/multilog/pyOptris/direct_binding.py new file mode 100644 index 0000000..4dff13d --- /dev/null +++ b/multilog/pyOptris/direct_binding.py @@ -0,0 +1,339 @@ +import sys +import ctypes +from typing import Tuple +from enum import Enum +import numpy as np + +DEFAULT_WIN_PATH = "..//..//irDirectSDK//sdk//x64//libirimager.dll" +DEFAULT_LINUX_PATH = "/usr/lib/libirdirectsdk.so" +lib = None + +# Function to load the DLL accordingly to the OS +def load_DLL(dll_path: str): + global lib + if sys.platform == "linux": + path = dll_path if dll_path is not None else DEFAULT_LINUX_PATH + lib = ctypes.CDLL(DEFAULT_LINUX_PATH) + + elif sys.platform == "win32": + path = dll_path if dll_path is not None else DEFAULT_WIN_PATH + lib = ctypes.CDLL(path) + + +# Load DLL +load_DLL(None) + +# +# @brief Initializes an IRImager instance connected to this computer via USB +# @param[in] xml_config path to xml config +# @param[in] formats_def path to Formats.def file. Set zero for standard value. +# @param[in] log_file path to log file. Set zero for standard value. +# @return 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_usb_init(const char* xml_config, const char* formats_def, const char* log_file); +# +def usb_init(xml_config: str, formats_def: str = None, log_file: str = None) -> int: + return lib.evo_irimager_usb_init( + xml_config.encode(), + None if formats_def is None else formats_def.encode(), + None if log_file is None else log_file.encode(), + ) + + +# +# @brief Initializes the TCP connection to the daemon process (non-blocking) +# @param[in] IP address of the machine where the daemon process is running ("localhost" can be resolved) +# @param port Port of daemon, default 1337 +# @return error code: 0 on success, -1 on host not found (wrong IP, daemon not running), -2 on fatal error +# +# __IRDIRECTSDK_API__ int evo_irimager_tcp_init(const char* ip, int port); +# +def tcp_init(ip: str, port: int) -> int: + return lib.evo_irimager_tcp_init(ip.encode(), port) + + +# +# @brief Disconnects the camera, either connected via USB or TCP +# @return 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_terminate(); +# +def terminate() -> int: + return lib.evo_irimager_terminate(None) + + +# +# @brief Accessor to image width and height +# @param[out] w width +# @param[out] h height +# @return 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_get_thermal_image_size(int* w, int* h); +# +def get_thermal_image_size() -> Tuple[int, int]: + width = ctypes.c_int() + height = ctypes.c_int() + _ = lib.evo_irimager_get_thermal_image_size( + ctypes.byref(width), ctypes.byref(height) + ) + return width.value, height.value + + +# +# @brief Accessor to width and height of false color coded palette image +# @param[out] w width +# @param[out] h height +# @return 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_get_palette_image_size(int* w, int* h); +# +def get_palette_image_size() -> Tuple[int, int]: + width = ctypes.c_int() + height = ctypes.c_int() + _ = lib.evo_irimager_get_palette_image_size( + ctypes.byref(width), ctypes.byref(height) + ) + return width.value, height.value + + +# +# @brief Accessor to thermal image by reference +# Conversion to temperature values are to be performed as follows: +# t = ((double)data[x] - 1000.0) / 10.0; +# @param[in] w image width +# @param[in] h image height +# @param[out] data pointer to unsigned short array allocate by the user (size of w * h) +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_get_thermal_image(int* w, int* h, unsigned short* data); +# +def get_thermal_image(width: int, height: int) -> np.ndarray: + w = ctypes.byref(ctypes.c_int(width)) + h = ctypes.byref(ctypes.c_int(height)) + thermalData = np.empty((height, width), dtype=np.uint16) + thermalDataPointer = thermalData.ctypes.data_as(ctypes.POINTER(ctypes.c_ushort)) + _ = lib.evo_irimager_get_thermal_image(w, h, thermalDataPointer) + return thermalData + + +# +# @brief Accessor to an RGB palette image by reference +# data format: unsigned char array (size 3 * w * h) r,g,b +# @param[in] w image width +# @param[in] h image height +# @param[out] data pointer to unsigned char array allocate by the user (size of 3 * w * h) +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_get_palette_image(int* w, int* h, unsigned char* data); +# +def get_palette_image(width: int, height: int) -> np.ndarray: + w = ctypes.byref(ctypes.c_int(width)) + h = ctypes.byref(ctypes.c_int(height)) + paletteData = np.empty((height, width, 3), dtype=np.uint8) + paletteDataPointer = paletteData.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) + retVal = -1 + while retVal != 0: + retVal = lib.evo_irimager_get_palette_image(w, h, paletteDataPointer) + return paletteData + + +# +# @brief Accessor to an RGB palette image and a thermal image by reference +# @param[in] w_t width of thermal image +# @param[in] h_t height of thermal image +# @param[out] data_t data pointer to unsigned short array allocate by the user (size of w * h) +# @param[in] w_p width of palette image (can differ from thermal image width due to striding) +# @param[in] h_p height of palette image (can differ from thermal image height due to striding) +# @param[out] data_p data pointer to unsigned char array allocate by the user (size of 3 * w * h) +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_get_thermal_palette_image(int w_t, int h_t, unsigned short* data_t, int w_p, int h_p, unsigned char* data_p ); +# +def get_thermal_palette_image(width: int, height: int) -> Tuple[int, int]: + w = ctypes.byref(ctypes.c_int(width)) + h = ctypes.byref(ctypes.c_int(height)) + thermalData = np.empty((height, width), dtype=np.uint16) + paletteData = np.empty((height, width, 3), dtype=np.uint8) + thermalDataPointer = thermalData.ctypes.data_as(ctypes.POINTER(ctypes.c_ushort)) + paletteDataPointer = paletteData.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) + _ = lib.evo_irimager_get_thermal_palette_image( + w, h, thermalDataPointer, w, h, paletteDataPointer + ) + return (thermalData, paletteData) + + +# +# @brief sets palette format to daemon. +# Defined in IRImager Direct-SDK, see +# enum EnumOptrisColoringPalette{eAlarmBlue = 1, +# eAlarmBlueHi = 2, +# eGrayBW = 3, +# eGrayWB = 4, +# eAlarmGreen = 5, +# eIron = 6, +# eIronHi = 7, +# eMedical = 8, +# eRainbow = 9, +# eRainbowHi = 10, +# eAlarmRed = 11 }; +# +# @param id palette id +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_set_palette(int id); +# +class ColouringPalette(Enum): + ALARM_BLUE = 1 + ALARM_BLUE_HI = 2 + GRAY_BW = 3 + GRAY_WB = 4 + ALARM_GREEN = 5 + IRON = 6 + IRON_HI = 7 + MEDICAL = 8 + RAINBOW = 9 + RAINBOW_HI = 10 + ALARM_RED = 11 + + +def set_palette(colouringPalette: ColouringPalette) -> int: + return lib.evo_irimager_set_palette(colouringPalette) + + +# +# @brief sets palette scaling method +# Defined in IRImager Direct-SDK, see +# enum EnumOptrisPaletteScalingMethod{eManual = 1, +# eMinMax = 2, +# eSigma1 = 3, +# eSigma3 = 4 }; +# @param scale scaling method id +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_set_palette_scale(int scale); +# +class PaletteScalingMethod(Enum): + MANUAL = 1 + MIN_MAX = 2 + SIGMA1 = 3 + SIGMA3 = 4 + + +def set_palette_scale(paletteScalingMethod: PaletteScalingMethod) -> int: + return lib.evo_irimager_set_palette_scale(paletteScalingMethod) + + +# +# @brief sets shutter flag control mode +# @param mode 0 means manual control, 1 means automode +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_set_shutter_mode(int mode); +# +class ShutterMode(Enum): + MANUAL = 0 + AUTO = 1 + + +def set_shutter_mode(shutterMode: ShutterMode) -> int: + return lib.evo_irimager_set_shutter_mode(shutterMode) + + +# +# @brief forces a shutter flag cycle +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_trigger_shutter_flag(); +# +def trigger_shutter_flag() -> int: + return lib.evo_irimager_trigger_shutter_flag() + + +# +# @brief sets the minimum and maximum remperature range to the camera (also configurable in xml-config) +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_set_temperature_range(int t_min, int t_max); +# +def set_temperature_range(min: int, max: int) -> int: + return lib.evo_irimager_set_temperature_range(min, max) + + +# +# @brief sets radiation properties, i.e. emissivity and transmissivity parameters (not implemented for TCP connection, usb mode only) +# @param[in] emissivity emissivity of observed object [0;1] +# @param[in] transmissivity transmissivity of observed object [0;1] +# @param[in] tAmbient ambient temperature, setting invalid values (below -273,15 degrees) forces the library to take its own measurement values. +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_set_radiation_parameters(float emissivity, float transmissivity, float tAmbient); +# +def set_radiation_parameters( + emissivity: float, transmissivity: float, ambientTemperature: float +) -> int: + return lib.evo_irimager_set_radiation_parameters( + ctypes.c_float(emissivity), + ctypes.c_float(transmissivity), + ctypes.c_float(ambientTemperature), + ) + + +# +# @brief +# @return error code: 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_to_palette_save_png(unsigned short* thermal_data, int w, int h, const char* path, int palette, int palette_scale); +# + +# +# @brief Set the position of the focusmotor +# @param[in] pos fucos motor position in % +# @return error code: 0 on success, -1 on error or if no focusmotor is available +# +# __IRDIRECTSDK_API__ int evo_irimager_set_focusmotor_pos(float pos); +# +def set_focus_motor_position(position: float) -> int: + return lib.evo_irimager_set_focusmotor_pos(position) + + +# +# @brief Get the position of the focusmotor +# @param[out] posOut Data pointer to float for current fucos motor position in % (< 0 if no focusmotor available) +# @return error code: 0 on success, -1 on error +# +# __IRDIRECTSDK_API__ int evo_irimager_get_focusmotor_pos(float *posOut); +# +def get_focus_motor_position() -> float: + position = ctypes.c_float() + _ = lib.evo_irimager_get_focusmotor_pos(ctypes.byref(position)) + return position.value + + +# +# Launch TCP daemon +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_daemon_launch(); +# +def daemon_launch() -> int: + return lib.evo_irimager_daemon_launch(None) + + +# +# Check whether daemon is already running +# @return error code: 0 daemon is already active, -1 daemon is not started yet +# +# __IRDIRECTSDK_API__ int evo_irimager_daemon_is_running(); +# +def daemon_is_running() -> int: + return lib.evo_irimager_daemon_is_running(None) + + +# +# Kill TCP daemon +# @return error code: 0 on success, -1 on error, -2 on fatal error (only TCP connection) +# +# __IRDIRECTSDK_API__ int evo_irimager_daemon_kill(); +# +def daemon_kill() -> int: + return lib.evo_irimager_daemon_kill(None) diff --git a/multilog/pyOptris/readme.md b/multilog/pyOptris/readme.md new file mode 100644 index 0000000..4a11924 --- /dev/null +++ b/multilog/pyOptris/readme.md @@ -0,0 +1,25 @@ +This code is copied from https://github.com/FiloCara/pyOptris/tree/dev/pyOptris + +There was no License file / text included but in the setup.py it is stated that this code is published under MIT License: + +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/multilog/view.py b/multilog/view.py new file mode 100644 index 0000000..31d1a3c --- /dev/null +++ b/multilog/view.py @@ -0,0 +1,974 @@ +"""This module contains the main GUI window and a class for each device +implementing respective the tab. + +Each device-widget must implement the following functions: +- def set_initialization_data(self, sampling: Any) -> None +- set_measurement_data(self, rel_time: list, meas_data: Any) -> None +""" + + +from functools import partial +import logging +from matplotlib.colors import cnames +import numpy as np +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QFont +from PyQt5.QtWidgets import ( + QMainWindow, + QWidget, + QHBoxLayout, + QVBoxLayout, + QGridLayout, + QComboBox, + QFrame, + QLineEdit, + QPushButton, + QSplitter, + QTabWidget, + QScrollArea, + QLabel, + QCheckBox, + QGroupBox, +) +import pyqtgraph as pg + +from .devices import ( + BaslerCamera, + Daq6510, + IfmFlowmeter, + Eurotherm, + OptrisIP640, + ProcessConditionLogger, + PyrometerArrayLumasense, + PyrometerLumasense, +) + + +logger = logging.getLogger(__name__) +COLORS = [ + "red", + "green", + "cyan", + "magenta", + "blue", + "orange", + "darkmagenta", + "yellow", + "turquoise", + "purple", + "brown", + "tomato", + "lime", + "olive", + "navy", + "darkmagenta", + "beige", + "peru", + "grey", + "white", +] + + +class LineEdit(QLineEdit): + """Modified of QLineEdit: red color if modified and not saved.""" + + def __init__(self, parent=None): + super(LineEdit, self).__init__(parent) + + def focusInEvent(self, e): + super(LineEdit, self).focusInEvent(e) + self.setStyleSheet("color: red") + self.selectAll() + + def mousePressEvent(self, e): + self.setStyleSheet("color: red") + self.selectAll() + + +class MainWindow(QMainWindow): + """multilog's main window.""" + + def __init__(self, start_function, exit_function, parent=None): + """Initialize main window. + + Args: + start_function (func): function to-be-connected to start button + exit_function (func): function to-be-connected to exit button + """ + super().__init__(parent) + self.start_function = start_function + self.exit_function = exit_function + + # Main window + self.setWindowTitle("multilog 2") + self.setWindowIcon(QIcon("./multilog/icons/nemocrys.png")) + self.resize(1400, 900) + self.move(300, 10) + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + self.main_layout = QVBoxLayout() + self.central_widget.setLayout(self.main_layout) + splitter_style_sheet = ( + "QSplitter::handle{background: LightGrey; width: 5px; height: 5px;}" + ) + self.splitter_display = QSplitter(Qt.Vertical, frameShape=QFrame.StyledPanel) + self.splitter_display.setChildrenCollapsible(False) + self.splitter_display.setStyleSheet(splitter_style_sheet) + self.main_layout.addWidget(self.splitter_display) + + # Tabs with instruments + self.tab_widget = QTabWidget() + self.tab_widget.setStyleSheet("QTabBar {font-size: 14pt; color: blue;}") + scroll_area = QScrollArea() + scroll_area.setWidget(self.tab_widget) + scroll_area.setWidgetResizable(True) + scroll_area.setFixedHeight(1000) + self.splitter_display.addWidget(self.tab_widget) + + # Buttons and labels with main information + self.button_widget = QWidget() + self.button_layout = QHBoxLayout() + self.button_widget.setLayout(self.button_layout) + self._setup_buttons(self.button_layout) + self.splitter_display.addWidget(self.button_widget) + self.lbl_current_time_txt = QLabel("Current time: ") + self.lbl_current_time_txt.setFont(QFont("Times", 12)) + self.lbl_current_time_val = QLabel("XX:XX:XX") + self.lbl_current_time_val.setFont(QFont("Times", 12, QFont.Bold)) + self.lbl_current_time_val.setStyleSheet(f"color: blue") + self.button_layout.addWidget(self.lbl_current_time_txt) + self.button_layout.addWidget(self.lbl_current_time_val) + self.lbl_start_time_txt = QLabel("Start time: ") + self.lbl_start_time_txt.setFont(QFont("Times", 12)) + self.lbl_start_time_val = QLabel("XX.XX.XXXX XX:XX:XX") + self.lbl_start_time_val.setFont(QFont("Times", 12, QFont.Bold)) + self.lbl_start_time_val.setStyleSheet(f"color: blue") + self.button_layout.addWidget(self.lbl_start_time_txt) + self.button_layout.addWidget(self.lbl_start_time_val) + self.lbl_output_dir_txt = QLabel("Output directory: ") + self.lbl_output_dir_txt.setFont(QFont("Times", 12)) + self.lbl_output_dir_val = QLabel("./measdata_XXXX-XX-XX_#XX") + self.lbl_output_dir_val.setFont(QFont("Times", 12, QFont.Bold)) + self.lbl_output_dir_val.setStyleSheet(f"color: blue") + self.button_layout.addWidget(self.lbl_output_dir_txt) + self.button_layout.addWidget(self.lbl_output_dir_val) + + def add_tab(self, tab_widget, tab_name): + """Add device tab to the main layout. + + Args: + tab_widget (QWidget): widget to-be added + tab_name (str): name of the tab + """ + self.tab_widget.addTab(tab_widget, tab_name) + + def _setup_buttons(self, button_layout): + self.btn_start = QPushButton() + self.btn_start.setText("Start") + self.btn_start.setMaximumWidth(300) + self.btn_start.setIcon(QIcon("./multilog/icons/Start-icon.png")) + self.btn_start.setFont(QFont("Times", 16, QFont.Bold)) + self.btn_start.setStyleSheet("color: red") + self.btn_start.setEnabled(True) + + self.btn_exit = QPushButton() + self.btn_exit.setText("Exit") + self.btn_exit.setMaximumWidth(380) + self.btn_exit.setIcon(QIcon("./multilog/icons/Exit-icon.png")) + self.btn_exit.setFont(QFont("Times", 16, QFont.Bold)) + self.btn_exit.setStyleSheet("color: red") + self.btn_exit.setEnabled(True) + + button_layout.addWidget(self.btn_start) + button_layout.addWidget(self.btn_exit) + button_layout.setSpacing(20) + + self.btn_start.clicked.connect(self.btn_start_click) + self.btn_exit.clicked.connect(self.btn_exit_click) + + def btn_start_click(self): + self.btn_exit.setEnabled(True) + self.btn_start.setEnabled(False) + self.lbl_current_time_val.setStyleSheet(f"color: red") + self.start_function() + + def btn_exit_click(self): + self.exit_function() + + def set_current_time(self, current_time): + self.lbl_current_time_val.setText(f"{current_time}") + + def set_output_directory(self, output_directory): + self.lbl_output_dir_val.setText(f"{output_directory}") + + def set_start_time(self, start_time): + self.lbl_start_time_val.setText(f"{start_time}") + + +class ImageWidget(QSplitter): + """Base class for devices displaying an image.""" + + def __init__(self, parent=None): + super().__init__(Qt.Horizontal, frameShape=QFrame.StyledPanel, parent=parent) + self.setChildrenCollapsible(True) + self.setStyleSheet( + "QSplitter::handle{background: LightGrey; width: 5px; height: 5px;}" + ) + + self.graphics_widget = QWidget() + self.graphics_layout = QVBoxLayout() + self.graphics_widget.setLayout(self.graphics_layout) + self.addWidget(self.graphics_widget) + + self.parameter_widget = QWidget() + self.parameter_layout = QGridLayout() + self.parameter_widget.setLayout(self.parameter_layout) + self.addWidget(self.parameter_widget) + + self.image_view = pg.ImageView() + self.graphics_layout.addWidget(self.image_view) + + def set_image(self, data): + """Set an image to be displayed. + + Args: + data (numpy.array): image + """ + self.image_view.setImage(data) + + def set_cmap(self, cmap_name="turbo"): + """Set color map for the image (if data is 2D heatmap)""" + self.cmap = pg.colormap.getFromMatplotlib(cmap_name) + self.image_view.setColorMap(self.cmap) + + +class PlotWidget(QSplitter): + """Base class for devices displaying a 2D plot.""" + + def __init__(self, sensors, parameter="Temperature", unit="°C", parent=None): + """Setup plot widget tab. + + Args: + sensors (list): list of sensors + parameter (str, optional): name of visualized parameter. + Defaults to "Temperature". + unit (str, optional): unit of visualized parameter. + Defaults to "°C". + """ + super().__init__(Qt.Horizontal, frameShape=QFrame.StyledPanel, parent=parent) + self.unit = unit + + # setup main layout + self.setChildrenCollapsible(True) + self.setStyleSheet( + "QSplitter::handle{background: LightGrey; width: 5px; height: 5px;}" + ) + + self.graphics_widget = QWidget() + self.graphics_layout = QVBoxLayout() + self.graphics_widget.setLayout(self.graphics_layout) + self.addWidget(self.graphics_widget) + + self.parameter_widget = QWidget() + self.parameter_layout = QGridLayout() + self.parameter_widget.setLayout(self.parameter_layout) + self.addWidget(self.parameter_widget) + + self.graphics_widget = pg.GraphicsLayoutWidget() + self.graphics_layout.addWidget(self.graphics_widget) + + # setup plot + self.plot = self.graphics_widget.addPlot() + self.plot.showGrid(x=True, y=True) + self.plot.setLabel("left", f"{parameter} [{unit}]") + self.plot.setLabel("bottom", "time [s]") + # self.plot.getAxis('top').setTicks([x2_ticks,[]]) # TODO set that! + self.plot.enableAutoRange(axis="x") + self.plot.enableAutoRange(axis="y") + self.pens = [] + for color in COLORS: + self.pens.append(pg.mkPen(color=cnames[color])) + self.lines = {} + for i in range(len(sensors)): + line = self.plot.plot([], [], pen=self.pens[i]) + self.lines.update({sensors[i]: line}) + + self.padding = 0.0 + self.x_min = 0 + self.x_max = 60 + self.y_min = 0 + self.y_max = 1 + + # setup controls for figure scaling + self.group_box_plot = QGroupBox("Plot confiuration") + # self.group_box_plot.setObjectName('Group') + # self.group_box_plot.setStyleSheet( + # 'QGroupBox#Group{border: 1px solid black; color: black; \ + # font-size: 16px; subcontrol-position: top left; font-weight: bold;\ + # subcontrol-origin: margin; padding: 10px}' + # ) + self.group_box_plot_layout = QGridLayout() + self.group_box_plot.setLayout(self.group_box_plot_layout) + self.parameter_layout.addWidget(self.group_box_plot) + self.parameter_layout.setAlignment(self.group_box_plot, Qt.AlignTop) + + self.lbl_x_edit = QLabel("Time [s] : ") + self.lbl_x_edit.setFont(QFont("Times", 12)) + self.lbl_x_edit.setAlignment(Qt.AlignRight) + self.edit_x_min = LineEdit() + self.edit_x_min.setFixedWidth(90) + self.edit_x_min.setFont(QFont("Times", 14, QFont.Bold)) + self.edit_x_min.setText(str(self.x_min)) + self.edit_x_min.setEnabled(False) + self.edit_x_max = LineEdit() + self.edit_x_max.setFixedWidth(90) + self.edit_x_max.setFont(QFont("Times", 14, QFont.Bold)) + self.edit_x_max.setText(str(self.x_max)) + self.edit_x_max.setEnabled(False) + self.cb_autoscale_x = QCheckBox("Autoscale X") + self.cb_autoscale_x.setChecked(True) + self.cb_autoscale_x.setFont(QFont("Times", 12)) + self.cb_autoscale_x.setEnabled(True) + + self.lbl_y_edit = QLabel(f"{parameter} [{unit}] : ") + self.lbl_y_edit.setFont(QFont("Times", 12)) + self.lbl_y_edit.setAlignment(Qt.AlignRight) + self.edit_y_min = LineEdit() + self.edit_y_min.setFixedWidth(90) + self.edit_y_min.setFont(QFont("Times", 14, QFont.Bold)) + self.edit_y_min.setText(str(self.y_min)) + self.edit_y_min.setEnabled(False) + self.edit_y_max = LineEdit() + self.edit_y_max.setFixedWidth(90) + self.edit_y_max.setFont(QFont("Times", 14, QFont.Bold)) + self.edit_y_max.setText(str(self.y_max)) + self.edit_y_max.setEnabled(False) + self.cb_autoscale_y = QCheckBox("Autoscale y") + self.cb_autoscale_y.setChecked(True) + self.cb_autoscale_y.setFont(QFont("Times", 12)) + self.cb_autoscale_y.setEnabled(True) + + self.group_box_plot_layout.addWidget(self.lbl_x_edit, 0, 0, 1, 1) + self.group_box_plot_layout.setAlignment(self.lbl_x_edit, Qt.AlignBottom) + self.group_box_plot_layout.addWidget(self.edit_x_min, 0, 1, 1, 1) + self.group_box_plot_layout.setAlignment(self.edit_x_min, Qt.AlignBottom) + self.group_box_plot_layout.addWidget(self.edit_x_max, 0, 2, 1, 1) + self.group_box_plot_layout.setAlignment(self.edit_x_max, Qt.AlignBottom) + self.group_box_plot_layout.addWidget(self.cb_autoscale_x, 0, 3, 1, 3) + self.group_box_plot_layout.setAlignment(self.cb_autoscale_x, Qt.AlignBottom) + self.group_box_plot_layout.addWidget(self.lbl_y_edit, 1, 0, 1, 1) + self.group_box_plot_layout.setAlignment(self.lbl_y_edit, Qt.AlignBottom) + self.group_box_plot_layout.addWidget(self.edit_y_min, 1, 1, 1, 1) + self.group_box_plot_layout.addWidget(self.edit_y_max, 1, 2, 1, 1) + self.group_box_plot_layout.addWidget(self.cb_autoscale_y, 1, 3, 1, 3) + + self.cb_autoscale_x.clicked.connect(self.update_autoscale_x) + self.cb_autoscale_y.clicked.connect(self.update_autoscale_y) + self.edit_x_min.editingFinished.connect(self.edit_x_min_changed) + self.edit_x_max.editingFinished.connect(self.edit_x_max_changed) + self.edit_y_min.editingFinished.connect(self.edit_y_min_changed) + self.edit_y_max.editingFinished.connect(self.edit_y_max_changed) + + # setup labels for sensors + self.group_box_sensors = QGroupBox("Sensors") + self.group_box_sensors_layout = QGridLayout() + self.group_box_sensors.setLayout(self.group_box_sensors_layout) + self.parameter_layout.addWidget(self.group_box_sensors) + self.parameter_layout.setAlignment(self.group_box_sensors, Qt.AlignTop) + + self.sensor_name_labels = {} + self.sensor_value_labels = {} + for i in range(len(sensors)): + lbl_name = QLabel() + lbl_name.setText(f"{sensors[i]}:") + lbl_name.setFont(QFont("Times", 12, QFont.Bold)) + lbl_name.setStyleSheet(f"color: {COLORS[i]}") + self.group_box_sensors_layout.addWidget(lbl_name, i, 0, 1, 1) + self.sensor_name_labels.update({sensors[i]: lbl_name}) + lbl_value = QLabel() + if self.unit == "-": + lbl_value.setText(f"XXX.XXX") + else: + lbl_value.setText(f"XXX.XXX {self.unit}") + lbl_value.setFont(QFont("Times", 12, QFont.Bold)) + lbl_value.setStyleSheet(f"color: {COLORS[i]}") + self.group_box_sensors_layout.addWidget(lbl_value, i, 1, 1, 1) + self.sensor_value_labels.update({sensors[i]: lbl_value}) + + def update_autoscale_x(self): + if self.cb_autoscale_x.isChecked(): + self.edit_x_min.setEnabled(False) + self.edit_x_max.setEnabled(False) + self.plot.enableAutoRange(axis="x") + else: + self.edit_x_min.setEnabled(True) + self.edit_x_max.setEnabled(True) + self.plot.disableAutoRange(axis="x") + + def update_autoscale_y(self): + if self.cb_autoscale_y.isChecked(): + self.edit_y_min.setEnabled(False) + self.edit_y_max.setEnabled(False) + self.plot.enableAutoRange(axis="y") + else: + self.edit_y_min.setEnabled(True) + self.edit_y_max.setEnabled(True) + self.plot.disableAutoRange(axis="y") + + def edit_x_min_changed(self): + self.x_min = float(self.edit_x_min.text().replace(",", ".")) + self.edit_x_min.setText(str(self.x_min)) + self.edit_x_min.setStyleSheet("color: black") + self.edit_x_min.clearFocus() + self.plot.setXRange(self.x_min, self.x_max, padding=self.padding) + self.calc_x2_ticks() + + def edit_x_max_changed(self): + self.x_max = float(self.edit_x_max.text().replace(",", ".")) + self.edit_x_max.setText(str(self.x_max)) + self.edit_x_max.setStyleSheet("color: black") + self.edit_x_max.clearFocus() + self.plot.setXRange(self.x_min, self.x_max, padding=self.padding) + self.calc_x2_ticks() + + def edit_y_min_changed(self): + self.y_min = float(self.edit_y_min.text().replace(",", ".")) + self.edit_y_min.setText(str(self.y_min)) + self.edit_y_min.setStyleSheet("color: black") + self.edit_y_min.clearFocus() + self.plot.setYRange(self.y_min, self.y_max, padding=self.padding) + + def edit_y_max_changed(self): + self.y_max = float(self.edit_y_max.text().replace(",", ".")) + self.edit_y_max.setText(str(self.y_max)) + self.edit_y_max.setStyleSheet("color: black") + self.edit_y_max.clearFocus() + self.plot.setYRange(self.y_min, self.y_max, padding=self.padding) + + def calc_x2_ticks(self): # TODO + """Not implemented. Intended to be used for a datetime axis.""" + # # calculate the datetime axis at the top x axis + # delta_t = int(self.x_max[u] - self.x_min[u]) + # x2_min = time_start + datetime.timedelta(seconds=self.x_min[u]) + # x2_max = (x2_min + datetime.timedelta(seconds=delta_t)).strftime('%H:%M:%S') + # x2_list = [] + # x2_ticks = [] + # for i in range(x2_Nb_ticks): + # x2_list.append((x2_min + datetime.timedelta(seconds=i*delta_t/(x2_Nb_ticks-1))).strftime('%H:%M:%S')) + # x2_ticks.append([self.x_min[u]+i*delta_t/(x2_Nb_ticks-1), x2_list[i]]) + # self.ax_X_2[u].setTicks([x2_ticks,[]]) + pass + + def set_data(self, sensor, x, y): + """Set data for selected sensor in plot. + + Args: + sensor (str): name of the sensor + x (list): x values + y (list): y values + """ + # PyQtGraph workaround for NaN from instrument + x = np.array(x) + y = np.array(y) + con = np.isfinite(y) + if len(y) >= 2 and y[-2:-1] != np.nan: + y_ok = y[-2:-1] + y[~con] = y_ok + self.lines[sensor].setData(x, y, connect=np.logical_and(con, np.roll(con, -1))) + if self.unit == "-": + self.sensor_value_labels[sensor].setText(f"{y[-1]:.3f}") + else: + self.sensor_value_labels[sensor].setText(f"{y[-1]:.3f} {self.unit}") + + def set_label(self, sensor, val): + """Set the label with current measurement value + + Args: + sensor (str): name of the sensor + val (str/float): measurement value + """ + if self.unit == "-": + self.sensor_value_labels[sensor].setText(f"{val:.3f}") + else: + self.sensor_value_labels[sensor].setText(f"{val:.3f} {self.unit}") + + +class Daq6510Widget(QWidget): + def __init__(self, daq: Daq6510, parent=None): + """GUI widget of Kethley DAQ6510 multimeter. + + Args: + daq (Daq6510): Daq6510 device including configuration + information. + """ + logger.info(f"Setting up Daq6510Widget for device {daq.name}") + super().__init__(parent) + self.layout = QGridLayout() + self.setLayout(self.layout) + self.tab_widget = QTabWidget() + self.layout.addWidget(self.tab_widget) + self.tab_widget.setStyleSheet("QTabBar {font-size: 14pt; color: blue;}") + + # create dicts with information required for visualization + self.tabs_sensors = {} # tab name : sensor name + self.sensors_tabs = {} # sensor name : tab name + self.tabs_units = {} # tab name : unit name + for channel in daq.config["channels"]: + sensor_type = daq.config["channels"][channel]["type"].lower() + sensor_name = daq.channel_id_names[channel] + if sensor_type == "temperature": + unit = "°C" + else: + if "unit" in daq.config["channels"][channel]: + unit = daq.config["channels"][channel]["unit"] + else: + unit = "V" + if "tab-name" in daq.config["channels"][channel]: + tab_name = daq.config["channels"][channel]["tab-name"] + if not tab_name in self.tabs_sensors: + self.tabs_sensors.update({tab_name: [sensor_name]}) + self.tabs_units.update({tab_name: unit}) + else: + self.tabs_sensors[tab_name].append(sensor_name) + if unit != self.tabs_units[tab_name]: + raise ValueError(f"Different units given for tab {tab_name}.") + else: + if sensor_type == "temperature": + tab_name = "Temperature" + if not tab_name in self.tabs_sensors: + self.tabs_sensors.update({tab_name: [sensor_name]}) + self.tabs_units.update({tab_name: unit}) + else: + self.tabs_sensors[tab_name].append(sensor_name) + elif sensor_type == "dcv": + tab_name = "DCV" + if not tab_name in self.tabs_sensors: + self.tabs_sensors.update({tab_name: [sensor_name]}) + self.tabs_units.update({tab_name: unit}) + else: + self.tabs_sensors[tab_name].append(sensor_name) + if unit != self.tabs_units[tab_name]: + raise ValueError( + f"Different units given for tab {tab_name}." + ) + elif sensor_type == "acv": + tab_name = "ACV" + if not tab_name in self.tabs_sensors: + self.tabs_sensors.update({tab_name: [sensor_name]}) + self.tabs_units.update({tab_name: unit}) + else: + self.tabs_sensors[tab_name].append(sensor_name) + if unit != self.tabs_units[tab_name]: + raise ValueError( + f"Different units given for tab {tab_name}." + ) + self.sensors_tabs.update({sensor_name: tab_name}) + + # create widgets for each tab + self.plot_widgets = {} + for tab_name in self.tabs_sensors: + plot_widget = PlotWidget( + self.tabs_sensors[tab_name], tab_name, self.tabs_units[tab_name] + ) + self.tab_widget.addTab(plot_widget, tab_name) + self.plot_widgets.update({tab_name: plot_widget}) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. + meas_data (dict): {sensor name: measurement time series} + """ + for sensor in meas_data: + self.plot_widgets[self.sensors_tabs[sensor]].set_data( + sensor, rel_time, meas_data[sensor] + ) + + def set_initialization_data(self, sampling): + """Update labels with sampling data (used before recording is + started). + + Args: + sampling (dict): {sensor name: value} + """ + for sensor in sampling: + self.plot_widgets[self.sensors_tabs[sensor]].set_label( + sensor, sampling[sensor] + ) + + +class IfmFlowmeterWidget(QWidget): + def __init__(self, flowmeter: IfmFlowmeter, parent=None): + """GUI widget of IFM flowmeter. + + Args: + flowmeter (IfmFlowmeter): IfmFlowmeter device including + configuration information. + """ + logger.info(f"Setting up IfmFlowmeterWidget for device {flowmeter.name}") + super().__init__(parent) + self.layout = QGridLayout() + self.setLayout(self.layout) + self.tab_widget = QTabWidget() + self.layout.addWidget(self.tab_widget) + self.tab_widget.setStyleSheet("QTabBar {font-size: 14pt; color: blue;}") + + # create dicts with information required for visualization + self.sensors = [] + for port in flowmeter.ports: + self.sensors.append(flowmeter.ports[port]["name"]) + + self.flow_widget = PlotWidget(self.sensors, "Flow", "l/min") + self.tab_widget.addTab(self.flow_widget, "Flow") + + self.temperature_widget = PlotWidget(self.sensors, "Temperature", "°C") + self.tab_widget.addTab(self.temperature_widget, "Temperature") + + def set_initialization_data(self, sampling): + """Update labels with sampling data (used before recording is + started). + + Args: + sampling (dict): { + "Flow": {sensor name: value}, + "Temperature": {sensor name: value}, + } + """ + for sensor in self.sensors: + self.temperature_widget.set_label(sensor, sampling["Temperature"][sensor]) + self.flow_widget.set_label(sensor, sampling["Flow"][sensor]) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. + meas_data (dict): { + "Flow": {sensor name: measurement time series}, + "Temperature": {sensor name: measurement time series}, + } + """ + for sensor in self.sensors: + self.temperature_widget.set_data( + sensor, rel_time, meas_data["Temperature"][sensor] + ) + self.flow_widget.set_data(sensor, rel_time, meas_data["Flow"][sensor]) + + +class EurothermWidget(QWidget): + def __init__(self, eurotherm: Eurotherm, parent=None): + """GUI widget of Eurotherm controller. + + Args: + flowmeter (Eurotherm): Eurotherm device including + configuration information. + """ + logger.info(f"Setting up EurothermWidget for device {eurotherm.name}") + super().__init__(parent) + self.layout = QGridLayout() + self.setLayout(self.layout) + self.tab_widget = QTabWidget() + self.layout.addWidget(self.tab_widget) + self.tab_widget.setStyleSheet("QTabBar {font-size: 14pt; color: blue;}") + + self.temperature_widget = PlotWidget(["Temperature"], "Temperature", "°C") + self.tab_widget.addTab(self.temperature_widget, "Temperature") + + self.op_widget = PlotWidget(["Operating point"], "Operating point", "-") + self.tab_widget.addTab(self.op_widget, "Operating point") + + def set_initialization_data(self, sampling): + """Update labels with sampling data (used before recording is + started). + + Args: + sampling (dict): {sampling name: value} + """ + self.temperature_widget.set_label("Temperature", sampling["Temperature"]) + self.op_widget.set_label("Operating point", sampling["Operating point"]) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. + meas_data (dict): {sampling name: measurement time series} + """ + self.temperature_widget.set_data( + "Temperature", rel_time, meas_data["Temperature"] + ) + self.op_widget.set_data( + "Operating point", rel_time, meas_data["Operating point"] + ) + + +class OptrisIP640Widget(ImageWidget): + def __init__(self, optris_ip_640: OptrisIP640, parent=None): + """GUI widget of Optirs Ip640 IR camera. + + Args: + optris_ip_640 (OptrisIP640): OptrisIP640 device including + configuration information. + """ + logger.info(f"Setting up OptrisIP640Widget for device {optris_ip_640.name}") + super().__init__(parent) + + def set_initialization_data(self, sampling): + """Update image with sampling data (used before recording is + started) using a grayscale colormap. + + Args: + sampling (np.array): IR image. + """ + self.set_image(sampling.T) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started) using turbo colormap. + + Args: + rel_time (list): relative time of measurement data. Unused. + meas_data (np.array): IR image. + """ + self.set_cmap("turbo") # TODO only do that once + self.set_image(meas_data.T) + + +class BaslerCameraWidget(ImageWidget): + def __init__(self, basler_camera: BaslerCamera, parent=None): + """GUI widget of Basler optical camera. + + Args: + basler_camera (BaslerCamera): BaslerCamera device including + configuration information. + """ + logger.info(f"Setting up BaslerCameraWidget for device {basler_camera.name}") + super().__init__(parent) + + def set_initialization_data(self, sampling): + """Update image with sampling data (used before recording is + started). + + Args: + sampling (np.array): image. + """ + self.set_image(np.swapaxes(sampling, 0, 1)) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. Unused. + meas_data (np.array): image. + """ + self.set_image(np.swapaxes(meas_data, 0, 1)) + + +class PyrometerLumasenseWidget(PlotWidget): + def __init__(self, pyrometer: PyrometerLumasense, parent=None): + """GUI widget of Lumasense pyrometer. + + Args: + pyrometer (PyrometerLumasense): PyrometerLumasense device + including configuration information. + """ + logger.info(f"Setting up PyrometerLumasense widget for device {pyrometer.name}") + self.sensor_name = pyrometer.name + super().__init__( + [self.sensor_name], parameter="Temperature", unit="°C", parent=parent + ) + + # Group box with emissivity, transmissivity, ... + self.group_box_parameter = QGroupBox("Pyrometer configuration") + self.group_box_parameter_layout = QVBoxLayout() + self.group_box_parameter.setLayout(self.group_box_parameter_layout) + self.parameter_layout.addWidget(self.group_box_parameter) + self.parameter_layout.setAlignment(self.group_box_parameter, Qt.AlignTop) + + self.lbl_emissivity = QLabel(f"Emissivity:\t{pyrometer.emissivity}%") + self.lbl_emissivity.setFont(QFont("Times", 12)) + self.group_box_parameter_layout.addWidget(self.lbl_emissivity) + self.lbl_transmissivity = QLabel( + f"Transmissivity:\t{pyrometer.transmissivity}%" + ) + self.lbl_transmissivity.setFont(QFont("Times", 12)) + self.group_box_parameter_layout.addWidget(self.lbl_transmissivity) + self.lbl_t90 = QLabel(f"t90:\t\t{pyrometer.t90} s") + self.lbl_t90.setFont(QFont("Times", 12)) + self.group_box_parameter_layout.addWidget(self.lbl_t90) + + def set_initialization_data(self, sampling): + """Update label with sampling data (used before recording is + started). + + Args: + sampling (float): temperature value + """ + self.set_label(self.sensor_name, sampling) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. + meas_data (list): measurement time series + """ + self.set_data(self.sensor_name, rel_time, meas_data) + + +class PyrometerArrayLumasenseWidget(PlotWidget): + def __init__(self, pyrometer_array: PyrometerArrayLumasense, parent=None): + """GUI widget of Lumasense pyrometer array. + + Args: + pyrometer_array (PyrometerArrayLumasense): + PyrometerArrayLumasense device including configuration + information. + """ + logger.info( + f"Setting up PyrometerArrayLumasense widget for device {pyrometer_array.name}" + ) + super().__init__( + pyrometer_array.sensors, parameter="Temperature", unit="°C", parent=parent + ) + + # Group box with emissivity, transmissivity, ... + self.group_box_parameter = QGroupBox("Pyrometer configuration") + self.group_box_parameter_layout = QVBoxLayout() + self.group_box_parameter.setLayout(self.group_box_parameter_layout) + self.parameter_layout.addWidget(self.group_box_parameter) + self.parameter_layout.setAlignment(self.group_box_parameter, Qt.AlignTop) + + for sensor in pyrometer_array.sensors: + lbl_emissivity = QLabel( + f"{sensor} emissivity:\t{pyrometer_array.emissivities[sensor]*100}%" + ) + lbl_emissivity.setFont(QFont("Times", 12)) + self.group_box_parameter_layout.addWidget(lbl_emissivity) + lbl_t90 = QLabel(f"{sensor} t90:\t\t{pyrometer_array.t90s[sensor]} s") + lbl_t90.setFont(QFont("Times", 12)) + self.group_box_parameter_layout.addWidget(lbl_t90) + + def set_initialization_data(self, sampling): + """Update labels with sampling data (used before recording is + started). + + Args: + sampling (dict): {head name: temperature} + """ + for sensor in sampling: + self.set_label(sensor, sampling[sensor]) + + def set_measurement_data(self, rel_time, meas_data): + """Update plot and labels with measurement data (used after + recording was started). + + Args: + rel_time (list): relative time of measurement data. + meas_data (dict): {heat name: measurement time series} + """ + for sensor in meas_data: + self.set_data(sensor, rel_time, meas_data) + + +class ProcessConditionLoggerWidget(QWidget): + def __init__(self, process_logger: ProcessConditionLogger, parent=None): + """GUI widget for logging of process conditions. + + Args: + process_logger (ProcessConditionLogger): + ProcessConditionLogger device including configuration + information. + """ + logger.info( + f"Setting up ProcessConditionLoggerWidget for device {process_logger.name}" + ) + super().__init__(parent) + self.process_logger = process_logger + self.layout = QGridLayout() + self.setLayout(self.layout) + + # create labels and input fields according to device configuration (read from config.yml) + self.input_boxes = {} + row = 0 + for condition in process_logger.config: + if "label" in process_logger.config[condition]: + label_name = QLabel(f"{process_logger.config[condition]['label']}:") + else: + label_name = QLabel(f"{condition}:") + label_name.setFont(QFont("Times", 12)) + self.layout.addWidget(label_name, row, 0) + + default_value = "" + if "default" in process_logger.config[condition]: + default_value = str( + process_logger.config[condition]["default"] + ).replace(",", ".") + if "values" in process_logger.config[condition]: + input_box = QComboBox() + input_box.addItems(process_logger.config[condition]["values"]) + input_box.setCurrentText(default_value) + input_box.currentIndexChanged.connect( + partial(self.update_combo_condition, condition) + ) + else: + input_box = LineEdit() + input_box.setText(default_value) + input_box.returnPressed.connect( + partial(self.update_text_condition, condition) + ) + input_box.setFixedWidth(320) + input_box.setFont(QFont("Times", 12)) + self.layout.addWidget(input_box, row, 1) + self.input_boxes.update({condition: input_box}) + + label_unit = QLabel(process_logger.condition_units[condition]) + label_unit.setFont(QFont("Times", 12)) + self.layout.addWidget(label_unit, row, 2) + + row += 1 + + def update_text_condition(self, condition_name): + """This is called if the text of an input filed was changed by + the user. The data in the ProcessConditionLogger is updated. + + Args: + condition_name (str): name of the changed process + """ + box = self.input_boxes[condition_name] + box.setStyleSheet("color: black") + box.clearFocus() + text = box.text().replace(",", ".") + box.setText(text) + self.process_logger.meas_data.update({condition_name: text}) + + def update_combo_condition(self, condition_name): + """This is called if the index of an input box was changed by + the user. The data in the ProcessConditionLogger is updated. + + Args: + condition_name (str): name of the changed process + """ + box = self.input_boxes[condition_name] + box.clearFocus() + self.process_logger.meas_data.update( + {condition_name: box.itemText(box.currentIndex())} + ) + + def set_initialization_data(self, sampling): + """This function exists to conform with the main sampling loop. + It's empty because no data needs to be visualized. + """ + pass + + def set_measurement_data(self, rel_time, meas_data): + """This function exists to conform with the main sampling loop. + It's empty because no data needs to be visualized. + """ + pass diff --git a/postprocessing/evaluate-daq6510.ipynb b/postprocessing/evaluate-daq6510.ipynb new file mode 100644 index 0000000..d7250bc --- /dev/null +++ b/postprocessing/evaluate-daq6510.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Evaluation of multilog measurement\n", + "\n", + "Growth experiment: Sn in aluminum crucible, September 8th, 2022\n", + "\n", + "## Process information\n", + "\n", + "- 11:29:11 - heating up\n", + "- 13:01:45 - seeding\n", + "- 13:08:15 - growing\n", + "- 21:01:43 - cool down\n", + "- 21:18:15 - end" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Read data and generate overview plots" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "df = pd.read_csv(\"./DAQ-6510.csv\", comment=\"#\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAEGCAYAAACKB4k+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAu6ElEQVR4nO3dd5xU1f3/8ddnC733LohYQBR0QQF7wxbRRBNiYokt+YYkmmJEExNLVEzxa75G448kRkxCsJeosaHYUHFBpAlSpUrvZWF3P78/7t1htg+wM3dm5/18PPYxM2fuvfM5s7v3c+85555r7o6IiAhATtQBiIhI+lBSEBGRGCUFERGJUVIQEZEYJQUREYnJizqAA9GuXTvv2bNn1GGIiGSUqVOnrnP39lW9l9FJoWfPnhQWFkYdhohIRjGzL6p7T81HIiISo6QgIiIxSgoiIhKjpCAiIjFKCiIiEqOkICIiMUoKIiISo6QgIvXGa7O/ZMuuPbHXb81bw4pNOyOMKPMoKYhIvfDF+u1c94+p/OTx6bGy7/z9Y4b/7zvRBZWBlBREpF7YuacEgGUbyp8ZbCsqjiKcjKWkICIZ7dK/fEjP0S9FHUa9oaQgIhlt8sL1UYdQryQ1KZjZEjObaWbTzawwLGtjZq+b2fzwsXXc8jeb2QIzm2dmw5MZm4jUT47j7ny5eVfUoWSkVJwpnOruA9y9IHw9Gpjo7n2AieFrzKwvMBLoB5wNPGRmuSmIT0TqAcNiz8e+s4jj75kYYTSZK4rmoxHAuPD5OODCuPIJ7l7k7ouBBcDg1IcnIpns89XbuOe/c6MOI2MlOyk48JqZTTWz68Kyju6+CiB87BCWdwWWxa27PCwrx8yuM7NCMytcu3ZtEkMXEck+yb7JzjB3X2lmHYDXzaym9G1VlHmlAvexwFiAgoKCSu+LiMj+S+qZgruvDB/XAM8SNAetNrPOAOHjmnDx5UD3uNW7ASuTGZ+I1B9W1WGl7LOkJQUza2pmzcueA2cBs4AXgCvCxa4Ang+fvwCMNLOGZtYL6ANMSVZ8IiJSWTKbjzoCz1qQvvOA8e7+ipl9DDxhZlcDS4FLANx9tpk9AcwBioFR7l6SxPhERKSCpCUFd18EHF1F+Xrg9GrWuQu4K1kxiYhIzXRFs4jUC+pSqBtKCiIiEqOkICIiMUoKIiISo6QgIvWCrlOoG0oKIlIvuOY3qBNKCiIiEqOkICIiMUoKIlIvqE+hbigpiIhIjJKCiIjEKCmIiEiMkoKI1BPqVKgLSgoiIhKjpCAiIjFKCiIiEqOkICL1gq5TqBtKCiIiEqOkICIiMUoKIiISo6QgIvWCuhTqhpKCiIjEKCmIiEiMkoKIiMQoKYhI2istdXbtKYk6jKyQF3UA6cDdKfxiIwe1aULThnlcPe5jTju8A/27tmLH7mIG9WpDi0b5seV37SlhyfrtNG+Uz+K122ndNJ9WTRqwbVcxjtO7fTNyzNi6aw8tGuVjBqUOuTlW7jNfnb2awb3a0KZpA7YVFfPJ0o18uXkXFw3sSl5u9fl6d3EpuTlWbnsi9dGrs7/ku/+YyqXH9WD8R0u5+6L+NG2Yy9De7fj7+4tp07RB1CHWO0oKwEszV/GD8Z+UK/tw0YaUfPbR3Vvx/KhhHHXbq5SGNx5ftmEHPznrsCqX37B9N8fc+ToAS8acl5IYJXm+3LyLUne6tGocdShp6cG3FgAw/qOlANzy7EwADm7XlEXrtkcWV32mpAA0bRjd19CrbROAWEIAWFjDH/vKTTuTHZKkwKYdu7l+wnTe/nwtoAS/r6pKCKZ5LuqEkgKQFzbDjL/mOA7r1Jxjf/MGAC/8YBj9u7Zk554SGufnUhQ22+Tn5vC7V+fy4FsLAZh265m1nsa6e+yP1t0pLnX6/OK/9G7fDID8XGNPide0iSDW3GAbajrKbD97ckYsIYikk6R3NJtZrpl9YmYvhq/bmNnrZjY/fGwdt+zNZrbAzOaZ2fBkx1ZRg7ycSkcbZkaTBnmYGY3yc8mv0NZ/6XE9EmrXjN+umZEbvi7xIBHEnylQe27APYGFJG1t3LE76hBEqpSK0UfXA5/FvR4NTHT3PsDE8DVm1hcYCfQDzgYeMrPcFMQXiZzwSH/Tjj1AhZ18AicBSgmZ6ZOlG1m4dlvUYWQMHfukXlKTgpl1A84D/hpXPAIYFz4fB1wYVz7B3YvcfTGwABiczPjSwaOTlwD7vpPXP0tmuuihyZz+h7c1JUMS6DutG8k+U7gf+DlQGlfW0d1XAYSPHcLyrsCyuOWWh2XlmNl1ZlZoZoVr19afNtl9PFGQDFf4xcaoQ8gI6jtOvaQlBTM7H1jj7lMTXaWKskrHw+4+1t0L3L2gffv2BxRjutJJgEhAZ8Spl8zRR8OAC8zsXKAR0MLM/gmsNrPO7r7KzDoDa8LllwPd49bvBqxMYnwiaSN+dJpIlJJ2puDuN7t7N3fvSdCB/Ka7fxt4AbgiXOwK4Pnw+QvASDNraGa9gD7AlGTFVz7WVHxKZcf0aAUEl/DH064h+zxZuDzqEDKecmrdiOI6hTHAE2Z2NbAUuATA3Web2RPAHKAYGOXuKZ3sJNV/VNOWbgLgPzN0QpTt5q3eGnUIIkCKkoK7TwImhc/XA6dXs9xdwF2piCmdzFqxOeFl1cYqIsmkWVLTwBfrd0QdgkRMyV7ShZJCGnhtzupyr3fsLmHNll0RRSNRcI05O2Cm3rg6sV9JwcwG1XUgstebc9cw+O6JtS63dP0O+v3qFRZrtkgRqSMJJwUz62tmd5jZfODPSYwpa9zz1f4HtP5z01ewfXcJT0/VyJVMp+ajA6ezrbpRY1Iws4PMbLSZfQr8A/g+cKa7F6Qkunpu5KDutS8kIpJC1SYFM5sMvAzkAxe7+7HAVndfkqLY6r26ulhJ47NF1KdQV2o6U1gLNAc6AmXzSdTL87P4SunPSkSyWbVJwd1HAP2BacDtZrYYaG1m9Xjm0sxMCWqPzny6P4akixovXnP3zcAjwCNm1gH4BnC/mXV3dzWIRywzU5hURSlB0kXCo4/cfY27P+DuQ4ETkhhTVunWWjdsF6kL6lurG/t1nYK7f1HXgWSrob3bRh2CpAG1Hkm60BXNEbvs+J77tLx2HvWTxthLulBSiNhhnZpHHYJI2lKyTL1aZ0k1s/bAtUDP+OXd/arkhZU9GuQpL4vOAOvCll17og6hXkhk6uzngXeBN4CU3t9AEqOjqcyn32DVqrsgzaxyIlVirRuJJIUm7n5T0iOJUKaOEddoC6nv9uVvPEP/jdNOIm0XL4b3Wa73Kv4Bpuqy+fHXHJeSzxERqU0iSeF6gsSw08y2mNlWM9uS7MCyydBD2h3Q+jpCynz6He4bnSQnT61Jwd2bu3uOuzd29xbh6xapCE4So2ak+kBZ4UDta9/a/NVbmbNy/49vl2/cwfai4tjr3cWlfLJ0435vL13UNEvq4eHjMVX9pC7E7HDCAZ4tiNRHdX288/z0FYz/aCm79pRw5v++w7n/9y4Q9Cs+/nFQXpG78+BbC2J3Q5y1YjNTv9jACfe+Rb9fv8rz01cA8JuX5nDRQ5NZuHZbjTG4e7l+THdnwZqa10mlmjqafwJcB/yhivccOC0pEWWpQzo0470F6/ZpHTU51B/6XVZt/fbdVZaXVvF9JfIdXj9hOgAzV2zau61S5825a7jp6Zk89sEXjBzcg8uOP4jdxaXc/fJnvD5nNSs27eSdz9fy+HeHcP4D71Xa5hlHdGTmis0AbNoRxLxq807aN2tIXm75Y+9R46fx8swvWTLmPFZs2snkBeu48akZnHBIO/6ZBv2L1SYFd78ufDw1deFkjrruhL7l3CN4dPKStIhFUk9JoWrLN+5MeNmlG3ZU+96dL87hZ2cdFnsd32x08cOT6da6CQCzV27h1udm8dwnK5j6RfmmoI8Wb2BWuOOvqN+vX2Vgj1YAfO3PH3BYx+bMW72Vq0/oRc+2TbjomG7k5xqT5q3l5ZlfAvDQpAX89pV5dG0VzH/23oJ1PPHxMjq0aMgph3VIuN51LZEhqZICiV7EVlW7qa5TkDJTFm/gsI7NadkkHwiOWk+89y0evWoQxx7UpsZ13Z3b/zOHbwzqzhGd93Ybzli+iS/W7+ArR3dJWtxvzl3N4Z1a0KXV/k8Q+cN/f1Lte397bzF/e29x7PWny/fu3Kct3cS0pZvKLV8xIZSpeJYQb/POvRfPzVu9Nfa5ALc+P7vS8r99ZR4AKzbtTXw/f3oGAEvGnFft5ySbLqcVSQMfLFpf6zKlpc6789fi7vzhtXnc+eIciopLKAnbUoqKS/j6//uA7zw6JbZO4ZKNbC0q5qG3FrJgzTZ27C5mzsot/OWdRZW2v3ZbEY9OXsJlf/uoXPkFf3q/0g532YYdsWaSeIVLNlBUXMLmnXtYGe7sthUVc9b/vl3uKHvClKWM/2gpELTzX/VoIRf86T1mLN8Ua28vqaqNKI0tWru9zrb17vy1dbatfaUzBSrceS0DW2LUfJRe3J3iUic/N/FjrqUbdvDAxPms21bE7SOOrLS9ax8rpNThzblr+N3FR/HAmwuA4Ej05EPbM+6qwbGd6LSlm3h++gpGDOga28aeUueM+97mxD7teHd+0Hd118uf8edvHcOQ3m3ZvruEwiUbAFi3bTeD7nqDcd8ZTN8ue88Ynp++gvOP6sIvn5vFv6cspWFeDu+PPo3xHy1l2CHt2F1cyjf/8iF9O7dgzqqgeWbJmPN44M35fL56G+c/8B6PXTWYE/u0Y/QzM4Hg6PreV+bGPveCP73Pt4/vQbfWTRjz37n79L3XJ5f9bQrv/vxUurdpkvLPTmTuo4nufnptZfVBNu5ad+wupu+vXuWPIweU24lEbcP23TTIy6FZw8p/olt37aFJgzxyc5L3G1u8bjvbi4o5smvLWNm2omI+WbqRE/u0r2FN+OVzs/jXR0srNQEsXV99mzfAH17/HCCWFJau30Hrpvms3LSLNz5bE1vuxqdmlFvv7c/X0vdXr7Bj996RM9dPmB7rVAV45/PgyLMsIZT5n39NqzKWtVuLOPf/3i13kHT9hOms3rKLf08JjvCLiksp+M0bANwXxg7EEgLAL56dyb/CMwKAyx+ZwtHd9n6nZQkh3j8/XFqpLBtt2L47vZKCmTUCmgDtzKw1e/eZLYDkNS5Kwuri5PrLzcEwu/te/zyypODuPPL+Ei4+thstGwdt4cfc+TpQvm118849PPz2Qv48aSHd2zTmL5cX0LRBXqV/nPfmr6NBXg6De+1tQ39/wTpaNs7nx49P5/6RA+jXpSXVufaxQl6fsxqAj245nSYNcmneKJ+bnp7BSzNWcd5Rnfn9xUdz4m/f5Mbhh/GNQT2A4J943OQlsZ2gu2NmsQRTU3t0vJ6jX0pouXjxCaEuVewAv/vlfTt6j08IZeLb86V6r8z+kqO7t0r559Z0pvBd4AaCBDCVvUlhC/BgcsPKTpN+dgqn/H5SwstvCIfr7S4prfTetqJimjXMY+byzRSXljKwR+sqt2HVtJf99IlPmbxwHZNHn0ZxqVNS6jTKz601pn9PWcrph3dg8N0TufS4Htx9UX/ufHEOe0pKuaNCs0hRcQk5Zrw2ezV3vjiH2Ss2c983BvBx2IxRZtHabUxfton3F6zn6WnLAVi2YSdn3x+MMX/rZ6ewp6SUnm2bcutzs3i8cNnede8+lxufmhFbD+DGJ2fw8vUnsmbLLn736jxuOPNQfvfKXJ6bvpJnvz80lhAAjrt7YqU6vjRjFS/NWAXATU/P5KRD29O5ZeNYIivz48en89z0lbV+ZyJVGTd5CTedfXjKP7emIal/BP5oZj909wdSGFPW6tmu6T4tXzaEdew7i+jeujGXDenJab+fRIvG+UxftqncskvGnMeThcvYXlTMyME9aJiXw9n3vxu70MZ97xHq7NuHx3aivW5+ObaNQT1b8+h3BnPvK3MZ2KMVJ/VpT9tmDSktdbYWFVNa6tz8zEx6tg2O3Md/tJT/Obl3bATGSX3aM2XJBsZW0ckJ8MwnK9i+u5hXZ+/dKSdy1HxqmEi7tGzEyvDMp8zBt7xcafk5q7aU2+6TU/cmjIsemlzr51U05J43qyxXQpADcVS36s9mk6nWPgV3f8DMhlL5fgqP1bRe2Pz0DtAwXO8pd/+1mbUBHg+3twT4urtvDNe5GbiaYIruH7n7q/tepdRKdj/E+m1FfLxkI1t27aFFo3xaNKr6V3br87OrHPZWZtiYN2ND3277z5xK78eP8e7366q/9o+XbIy999gH1d+RdUlc2/mJv30r9vyaxwqrXadMfELYVxUTgkgm6xFBfwIk1tH8D6A3MJ2991NwoMakABQBp7n7NjPLB94zs/8CXwUmuvsYMxsNjAZuMrO+wEigH0GT1Rtmdqi7Z9U9HK4/vQ9/nDgfCNqkjw078g5U/FhokWwzuFcbtu0qLtcJXlHHFg1ZvaWo0vNUys819pQEHTkn1DKgIVkSGTNXAAxz9++7+w/Dnx/VtpIHyib0yA9/HBgBjAvLxwEXhs9HABPcvcjdFwMLgMGJV6V+iB9tc8Z9b0cYiUj9MPfOs/n3tcfznWE9q3x/xm1nsWTMeXx0yxmxsrLnLRrlceGAYFzNef07M7xfR+7/xoBy63dq0Yg5dwznxuGHUZXbvtIXgHOO7MRvLizfr1ZxnW6tmzD2smMByI1ofHwi1ynMAjoBq/Z142aWS9BJfQjwoLt/ZGYd3X0VgLuvMrOy67m7Ah/Grb48LKu4zesI5mSiR48e+xpS1dLoGpkWjff+ShbW4cUwIvXNorvP5R8ffsGvX5jNHSP68a8Pl8auJC5zxhEdYgMkjuvVFoA7R/TjsiE9Y/1KLRrlx5afduuZ7AkHbnz+m3PIMdiwYzeFX2zkp2cdysHtmwFww+PTAZj405PpHZaNOvUQRp16SLn+qme/P5SBPVpz5bBesbKx7yxi6YYdvPWzU+jVrimPvLc4NsfT90/pHetbjGqmgkTOFNoBc8zsVTN7oewnkY27e4m7DwC6AYPN7MgaFq8qLVb6Vtx9rLsXuHtB+/Z1e3pV3UicVGrcQNcTZquy4bgVVXWtRl069qDWPPzt1Ex83LVV41qncOjSslHs+dw7z6Zt0wZcPuSgcsu8+MMTyMkxrhjakyVjzuPyIT15btSwStuKH5bco20Tlow5j8uG9ATg/112LP+8uvwEdG2aNqBji+DzG+TlkJebQ4fmjXjvptNiCQH2/k56x5WVmTz6NM7r35n/Xn9ilaP+vn9KbyBoogL448iBAIy7ajCXFHSP/HqpRP7abjvQD3H3TWY2CTgbWG1mncOzhM5A2VU5y4Hucat1A7Ju+Eb8cEipnwb1bM3HS/bOrfPIlQWc1Kc9ebk5vDFnNYd1as5b89bwq3DgwH+vP5HxU5YybvKSGq9HuOz4gzju4Db88Y35zI+binn0OYdTcFBrLn74Aw7v1JwLB3aNXS3cq11TnvreEMyMN35yMmfc9zb/c0pv/jxpIQCHd2rO3C/LH30/cmUBVz1ayCmHtWfsZQW8O38tV48LBhHcen5f7nyx8kCG7550MIN6tqFf1/K3Ypl9+3AWr9vO+Q+8x+mHdyA/N4f/++ZADv3lfwFolJ/L1FvPDL6HWV9y0cCu3HLuEVXWv1F+5WPcmq72H96vU7Xv1ebtG09hU9xcR/G6tGrMg9+qPsmOHNyDkYP3tnKc0KddpHMdVZTI6KO3zewgoI+7v2FmTYBaB6ybWXtgT5gQGgNnAPcCLwBXAGPCx+fDVV4AxpvZfQQdzX2AKZU2XM8dyFHCzecczj3VTA3QsnF+uQm7qnLv1/pTUgoHtW3Ct/4azH/TvU1jlm3Y20n91PeG0LFFI1o0zufNuau59bnZbCsqZsotpzM4HNP/7PeHsr2ohG9XmEMnmcZfcxyX/rXy50247nhGjv2wijWqdlDbJizfuJNHrhzEFY8k/ufXo02TSrN0Du3dlvlrtnFE5xaxK4qXjDkPd+d3r86jcMlG/nZlAc3jmi/O6NsRgMuH9OTy8IgW4KazD2fWis28O38d464aTLfWjZn2xUa27irmjhfn0CAvhzvD9urh/TpRUuqs3VpEcanTq11Tlm8MYuvTsTnfO7k33zu5d6U6HNKhGR/efDodmjfkooFd6dGmCY3yc2PNIe/ceCpNGuZSGl7RdtnxB9EgL4fTj+jIvV/rT6eWjTn50Pb07dyCFo3zOKhtUz5YuJ5rHyvk8M7NY3WL17RhHkd2bVlpp3j5kIMqjXD7+BdnkKixlx3LjU/N4NyjOie8zr5o26whbZs1TMq2y0Q1c24io4+uJWjDb0MwCqkr8DBQ2zQXnYFxYb9CDvCEu79oZh8AT5jZ1cBS4BIAd59tZk8Ac4BiYFS2jTwC6NelBS98WvsJ0vhrjqNhfi5f+/PecfXXnXRwuaRwVt+OvDZnNWf27chfLi/g7+8v5vb/zOH8ozrz4ozKXURlV+ZC0Lb64aL1nNu/M2ff/w5zv9zKwrvPLTe1xEUDu3HRwG6x1++PPo3XZn9Z7YVyiTiqW0tmVHPF68XHduOp8JqCvp1bsHNPCQ3zcrjroiM59qA2LLr7XG59fhY7dpfw7CfBjU+OP7gtT35vCJc8/AEA3xzcg3u+2r9cu2+Xlo3Co/O1XHB0F356VuUOwz+OHMAnSzfFrg0Zf81xDD2kHW/MWU1+Xg4nH9qenzwxnWemrYitM/7a42PP73ppDuu2Be3GZsbP9+OipBZh81J+rtG7fbNY08XFBd3IiWv6zM/NIT+Xcld6d2vdhAnXHU//rjWPfe8UNt0c2rF5pfd6tN27vYo78fi/nSG928aen9m3I6/ecBKHdqzczFKTO0YcWelix0QN6tmas/p14qwDOBOIUtSt2Ik0H40iGAX0EYC7z4/rHK6Wu88ABlZRvp5qEoq73wXclUBM9dYVQ3uW27HHT2AGcHa/ThT0bM3QQ9qVm9530d3nxvpEOjRvyCNXDio3bw8Qm6Cta6vGnN2vE62b5nPPV4/itN9PYtG68p3abZo24Nz+wVHW498dwuotu2qda6hrq8Z8J65DrXF+Lt3bNObz1UFTxnOjhnHZ3z5i667icuv96dKBnHNkZybNW8Nph3eIXTBXcFBrNu/cE2sKGfPV/gw7pC0/fvxT+nZpwe8vObrcdnJyjLsu6s/qLbt49pMV3DGiHwCDeraJnSk1rDBF+eJ7gu9t/uqtvDVvbazO8cp2gCMGdOWX5x3Byk27YjvI+KPfrx3TrVxSiPeL8/rW+N0l4u4L+3NU15YMObhtufL4jtKaHF9hvUTNun34AZ3BHtapcoJ562en0KRB7VfI7wsz4+UfnUj3Nvs//XY6iWr8SyJJocjdd5ftcMwsj7Qar1O/VJxK4i+XF9QwvUTwa+jWujE54Q57/LXH0bt9s1hnWbyvF3Rn5aadjDr1EJrGdV4+94NhbN5RfdNSy8b51XaC1mTOHcOBYKqINVuLGNC9FYW/PIOC37zB907uzcOTFvLy9SfGjmhPPyLYwc66fThzV22hb5cW7NxdwoSPl9GheXAHq4sGdqND80YcU8PZSMcWjSodyd59UX9GjZ9G57hOzCEHt40l0j4dm1da59bz+9Kmafl65+XmlDtijjfskKBt+IOF61lQyy0Z90fLJvl8t4pmn2RLRkd3r328ej9R8bO6Zq5oTxUS+W2/bWa3AI3N7Ezg+8B/khuWlKlpvqGyNsf4BDC0d/X3em6Ql1Nls0VwpfS+7/RrU7bDffvGUykuDYb5NczLZeZtQbIYdeohVa7XrGEeBT2DUSNNGuRVWm7YftzP+tz+nRh72bGxxPPJrWfSpGHNR6pXn9CrxverM6R323JNKCL7wyPqVEhkSOpNwFpgJsEkeS8Dv0xmUFK/NA5nGY2SmXFWv06xJrDWTRvQMK9umy9E6kJa9ymYWQ4ww92PBP6SmpBSL/4ikUy6YY3a8ESkrtV4puDupcCnZlZHlw6nt8xJB+VlatwiUlnU/8+J9Cl0Bmab2RQgNkTF3S9IWlRpIurTuNpENY5ZRJIvba9TAG5PehSyX8o6otI9eYlI4qKebieRPoUHwz4FSVOZ1A8iIulNfQpp6M4LE8vBaj0Sqb/SeZbUsj6Fifs6S6rsn7L522tzWDgVwVX7OZ5eRNJP1Of96lPIYK2bNkir2RVFpO6kbUezu+v2XyIiKRL1wJFEZkndyt7m6wYEt9Xc7u71YZIRQEM7RUTKJHKmUG6KQzO7kHp67+SoM7SISJmoDlYT6Wgux92fA06r+1CkTNTjlEUkOlEPMU+k+eircS9zgAI0GlJnFSJSLyUy+ugrcc+LgSXAiKREIyIiQBrfZMfdv5OKQEREJPpWiFr7FMxsnJm1invd2sweSWpUIiISiUQ6mo9y901lL9x9I1Xce1lEROpOOt95LcfMYjfENbM2JNYXISIiGSaRnfsfgMlm9hRB38fXgbuSGlWKlUvIGlUkIlkskY7mx8yskODaBAO+6u5zkh5ZBKIeHywiUiZtRx8BhEmgXiaCdKTUJJK90n70kYiIZA8lhTQW9RGDiGSfGpOCmeWa2RupCkZERELpOCGeu5cAO8ysZYrikTg6URDJPlFPiJlIR/MuYKaZvQ5sLyt09x8lLSoREYlEIknhpfBnn5hZd+AxoBNQCox19z+GF789DvQkmFzv6+FV0pjZzcDVQAnwI3d/dV8/d198umwT89dso1Xj/GR+jIjIPvOI2o8SuU5hnJk1Bnq4+7x92HYx8FN3n2ZmzYGp4dnGlcBEdx9jZqOB0cBNZtYXGAn0A7oAb5jZoWETVlKMePB9AP5yecF+byOZZ3pRn0aKSOpF/V+fyIR4XwGmA6+ErweY2Qu1refuq9x9Wvh8K/AZ0JVg2u1x4WLjgAvD5yOACe5e5O6LgQWk6A5vJaVBRs7RWCwRyXKJ7AZvI9g5bwJw9+lAr335EDPrSTCJ3kdAR3dfFW5rFdAhXKwrsCxuteVhWcVtXWdmhWZWuHbt2n0Jo1ql4TwXuTlR5+iAThBEJJ1vx1ns7psrlCUcrpk1A54GbnD3LTUtWkVZpc9x97HuXuDuBe3bt080jBrFkoL2xiISsah3Q4kkhVlmdimQa2Z9zOwBYHIiGzezfIKE8C93fyYsXm1mncP3OwNrwvLlQPe41bsBKxP5nANV1nykNnwRyXaJJIUfEnT+FgHjgc3A9bWtZMEe9m/AZ+5+X9xbLwBXhM+vAJ6PKx9pZg3NrBfQB5iSSCUOVLo1H5VJr2hEJJXSeUK889z9F8AvygrM7BLgyVrWGwZcRnCNw/Sw7BZgDPCEmV0NLAUuAXD32Wb2BMHEe8XAqGSOPIpXUho8qvlIRKIW9WzNiSSFm6mcAKoqK8fd36P6g93Tq1nnLiK4V8PmnXuA9Bt9pBwlIqlWbVIws3OAc4GuZvZ/cW+1IDiSrzfufDGYFTzdmo9EJHtFNfqopjOFlUAhcAEwNa58K/DjZAYVlQa5OWl1dB71aaSIpF7U+6Bqk4K7fwp8ambj3X1PCmNKmZGDujPh4+DSiD4dmtG2WUO27EqfqkZ1mbuIZK9E+hR6mtk9QF+gUVmhux+ctKhSqGOLhnx0yxlRh1GOzhBEJCqJdK3+HfgzQT/CqQST3P0jmUFJQMlBJHtF1VKQSFJo7O4TAXP3L9z9NuC05IYlIpKdoj4UTOh+CmaWA8w3sx8AK9g7X5EkU9R/HSKSdRI5U7gBaAL8CDgW+DZ7r0gWEZEkSMchqQC4+8cAZubu/p3kh5QZdBAvIkmR7hPimdkQM5tDcD8EzOxoM3so6ZFJ1H8bIpKFEmk+uh8YDqyH2PULJyUxJhGRrBfVVUoJzfbj7ssqFKVkorpki6rNTkSkOlEPRU9k9NEyMxsKuJk1IOhw/iy5YaVO1L8AEZF0ksiZwveAUQS3xlwODAhfS5JFPQeKiEQooqaMREYfrQO+lYJYRESyXtQHgzVNnf0ANfR1uPuPkhKRaCI8EYlMTWcKhXHPbwd+neRYpAL1d4hkr7S7Hae7jyt7bmY3xL+W5NKoKJHsFfWhYKI3oMya3VTUv5B4Ubctikj2SbO7EgtkUQYWkWql3dxHZraVvfunJma2pewtwN29RbKDExHJNhZxE0FNfQrNUxlIFGob5RPV70atRiISlaxvPkrHdns1H4lIVLI+KaSzNMxXIpIiHlGngpJCGorqj0FEohf1waCSQhqLusNJRLKPkoKISBpK6/spZKuopplQ45FI9oq6gUBJIY2p8UhEUi1pScHMHjGzNWY2K66sjZm9bmbzw8fWce/dbGYLzGyemQ1PVlyZoEFu8GsZ3KtNxJGISFSiGm+SzDOFR4GzK5SNBia6ex9gYvgaM+sLjAT6hes8ZGa5SYwtrTXKz+W1H5/Eny49JupQRCTFop4dOWlJwd3fATZUKB4BlM22Og64MK58grsXuftiYAEwOFmx7Y0x2Z+w/w7t2JzGDbI2L4pIRFLdp9DR3VcBhI8dwvKuwLK45ZaHZZWY2XVmVmhmhWvXrj3ggGrKyYl0+ESd1UWkfsr20UdV7Vmr/E7cfay7F7h7Qfv27ZMclohIimXZ6KPVZtYZIHxcE5YvB7rHLdcNWJni2EREsl6qk8ILwBXh8yuA5+PKR5pZQzPrBfQBpqQ4NhGRtBHVdDc13aP5gJjZv4FTgHZmtpzgHs9jgCfM7GpgKXAJgLvPNrMngDlAMTDK3UuSFVstcUfxsSIiQPQXryUtKbj7N6t56/Rqlr8LuCtZ8YiISO3SpaM5LUWdsUVEUk1JQUQkjUR9LKqksL90GiEi9VBWJ4U0vqBZRCQSWZ0UQKONRCQ91ccJ8TKeprAQkVSL+kBVSUFERGKUFERE0pBH1OuppCAikkaibrRWUhARkRglBRGRNKTRR2lIo1VFJNWi3u9kdVJI59txiohEIauTgohIusr223GKiAjRXzSrpCAiIjFKChVYNc9FRFJJo49ERESjj0REJH0oKYiIpCHNfSQiIpFTUhARkZisTgq1nZ5F3eEjIpJqWZ0UQDt+EUlPGpIqIiKRH6gqKYiISIySgoiIxCgp1EgdDiKSWpoQT0RE0kbaJQUzO9vM5pnZAjMbHXU8IiJR8IiGH6VVUjCzXOBB4BygL/BNM+sbbVQiIqkT9eijvGg/vpLBwAJ3XwRgZhOAEcCcuvyQouISHpv8Bc9MW7Hf2ygtDbL4rj0ldRWWiEjM71/7nN+/9nmV7+XmGC/96AQO79Sizj83rc4UgK7AsrjXy8OyOjV75RbuevmzKt/LzdmbphvkVv/1vLdgHQCFX2ys2+BEJKvlJHCqUFLqnH3/u8n5/KRsdf9V9W2Ua1gzs+vMrNDMCteuXbtfH3Jox+ZcelwPAG44o0+59xrl5/KVo7tw0qHt6dG2SbXbGPO1/gD885rj9isGEZGq5OYYIwZ0qXW5f16dnH2PRdWZURUzGwLc5u7Dw9c3A7j7PVUtX1BQ4IWFhSmMUEQk85nZVHcvqOq9dDtT+BjoY2a9zKwBMBJ4IeKYRESyRlp1NLt7sZn9AHgVyAUecffZEYclIpI10iopALj7y8DLUcchIpKN0q35SEREIqSkICIiMUoKIiISo6QgIiIxSgoiIhKTVhev7SszWwt8cQCbaAesq6Nwoqa6pK/6VJ/6VBeoX/XZl7oc5O7tq3ojo5PCgTKzwuqu6ss0qkv6qk/1qU91gfpVn7qqi5qPREQkRklBRERisj0pjI06gDqkuqSv+lSf+lQXqF/1qZO6ZHWfgoiIlJftZwoiIhJHSUFERGKyMimY2dlmNs/MFpjZ6KjjSYSZPWJma8xsVlxZGzN73czmh4+t4967OazfPDMbHk3UVTOz7mb2lpl9Zmazzez6sDzj6mNmjcxsipl9Gtbl9rA84+pSxsxyzewTM3sxfJ3JdVliZjPNbLqZFYZlGVkfM2tlZk+Z2dzwf2dIUuri7ln1Q3CfhoXAwUAD4FOgb9RxJRD3ScAxwKy4st8Co8Pno4F7w+d9w3o1BHqF9c2Nug5xcXcGjgmfNwc+D2POuPoQ3EK2Wfg8H/gIOD4T6xJXp58A44EXM/nvLIxxCdCuQllG1gcYB1wTPm8AtEpGXbLxTGEwsMDdF7n7bmACMCLimGrl7u8AGyoUjyD4QyF8vDCufIK7F7n7YmABQb3Tgruvcvdp4fOtwGdAVzKwPh7YFr7MD3+cDKwLgJl1A84D/hpXnJF1qUHG1cfMWhAcGP4NwN13u/smklCXbEwKXYFlca+Xh2WZqKO7r4JgRwt0CMszpo5m1hMYSHCEnZH1CZtbpgNrgNfdPWPrAtwP/BwojSvL1LpAkKBfM7OpZnZdWJaJ9TkYWAv8PWza+6uZNSUJdcnGpGBVlNW3cbkZUUczawY8Ddzg7ltqWrSKsrSpj7uXuPsAoBsw2MyOrGHxtK2LmZ0PrHH3qYmuUkVZWtQlzjB3PwY4BxhlZifVsGw61yePoPn4z+4+ENhO0FxUnf2uSzYmheVA97jX3YCVEcVyoFabWWeA8HFNWJ72dTSzfIKE8C93fyYsztj6AISn85OAs8nMugwDLjCzJQTNqqeZ2T/JzLoA4O4rw8c1wLMETSiZWJ/lwPLwLBTgKYIkUed1ycak8DHQx8x6mVkDYCTwQsQx7a8XgCvC51cAz8eVjzSzhmbWC+gDTIkgviqZmRG0jX7m7vfFvZVx9TGz9mbWKnzeGDgDmEsG1sXdb3b3bu7ek+D/4k13/zYZWBcAM2tqZs3LngNnAbPIwPq4+5fAMjM7LCw6HZhDMuoSdY96RL345xKMeFkI/CLqeBKM+d/AKmAPwVHA1UBbYCIwP3xsE7f8L8L6zQPOiTr+CnU5geBUdgYwPfw5NxPrAxwFfBLWZRbwq7A84+pSoV6nsHf0UUbWhaAd/tPwZ3bZ/3oG12cAUBj+rT0HtE5GXTTNhYiIxGRj85GIiFRDSUFERGKUFEREJEZJQUREYpQUREQkRklB0pKZlYQzW84ys/+UXQtQw/K3mdnPalnmQjPrG/f6DjM7ow5ivdLMusS9/mv859QVM5tc19tM5nYlMykpSLra6e4D3P1IgokAR9XBNi8kmD0SAHf/lbu/UQfbvRKIJQV3v8bd59TBdstx96F1vc1kblcyk5KCZIIPCCfzMrPeZvZKOMHZu2Z2eMWFzexaM/vYgnscPG1mTcxsKHAB8LvwDKS3mT1qZheb2Tlm9kTc+qeY2X/C52eZ2QdmNs3Mngzna4r/rIuBAuBf4XYbm9kkMysI399mZveG8b5hZoPD9xeZ2QXhMrlm9rsw5hlm9t2qvgQz2xYX3yTbO7f+v8KrxCsuP8nM/tfM3rFg/v1BZvaMBXPv/2Z/tyv1m5KCpDUzyyW4pL9sKpKxwA/d/VjgZ8BDVaz2jLsPcvejCablvtrdJ4fbuDE8A1kYt/zrwPHhVAgA3wAeN7N2wC+BMzyYVK2Q4F4DMe7+VFj+rXC7OyvE0hSYFMa7FfgNcCZwEXBHuMzVwGZ3HwQMAq4NpyaoyUDgBoIzn4MJ5i2qym53Pwl4mGAKhFHAkcCVZtb2ALYr9VRe1AGIVKOxBdNR9wSmAq+HR+lDgSfjDmAbVrHukeGRcCugGfBqTR/k7sVm9grwFTN7iuB+Aj8HTibYOb4ffl4DgrOWfbEbeCV8PhMocvc9ZjYzrBsEc/IcFZ51ALQkmKtmcQ3bneLuywHivqf3qliuLJnOBGZ7OM2ymS0imDBt/X5uV+opJQVJVzvdfYCZtQReJDjCfRTY5ME01TV5FLjQ3T81sysJ5vGpzePhZ2wAPnb3rWHTyevu/s39qkFgj++dS6YUKAJw91IzK/v/M4KznxqTVwVFcc9LqP5/uWy5UsqvU1rNOoluV+opNR9JWnP3zcCPCJqKdgKLzewSCGZbNbOjq1itObDKgum5vxVXvjV8ryqTCKYivpYgQQB8CAwzs0PCz2tiZodWsW5N203Eq8D/hPFiZofGNWWJpJSSgqQ9d/+EYKbLkQQ7+avNrGzmy6pupXorwZ3cXieYxrrMBOBGC+5c1bvCZ5QQnJGcEz7i7msJRhb928xmECSJSh3bBGcmD5d1NO9HFf9KMA3yNDObBfw/dIQuEdEsqSIiEqMzBRERiVFSEBGRGCUFERGJUVIQEZEYJQUREYlRUhARkRglBRERifn/8tJ4hCS1GPgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.plot(df[\"time_rel\"] / 60, df[\"rogowski_300A\"])\n", + "ax.set_xlabel(\"Relative time in min\")\n", + "ax.set_ylabel(\"Heater current in A\")\n", + "\n", + "fig2, ax2 = plt.subplots()\n", + "ax2.plot(df[\"time_rel\"] / 60, df[\"TE_2_K crucible frontside\"], label=\"front\")\n", + "ax2.plot(df[\"time_rel\"] / 60, df[\"Pt-100_1 crucible backside\"], label=\"back\")\n", + "ax2.plot(df[\"time_rel\"] / 60, df[\"Pt-100_2 crucible rightside\"], label=\"right\")\n", + "ax2.set_xlabel(\"Relative time in min\")\n", + "ax2.set_ylabel(\"Crucible temperature in °C\")\n", + "ax2.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluate measurement data during growth" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean current: 306.84 A\n", + "Mean temperature front: 242.75 °C\n", + "Mean temperature back: 241.81 °C\n", + "Mean temperature right: 240.71 °C\n" + ] + } + ], + "source": [ + "t_growth_start = 6129 # from protocol file\n", + "t_growth_end = 34533 # from protocol file\n", + "\n", + "# filter data\n", + "df_growth = df.loc[(t_growth_start < df[\"time_rel\"]) & (df[\"time_rel\"] < t_growth_end)]\n", + "\n", + "# compute average values\n", + "print(f\"Mean current: {df_growth['rogowski_300A'].mean():.2f} A\")\n", + "print(f\"Mean temperature front: {df_growth['TE_2_K crucible frontside'].mean():.2f} °C\")\n", + "print(f\"Mean temperature back: {df_growth['Pt-100_1 crucible backside'].mean():.2f} °C\")\n", + "print(f\"Mean temperature right: {df_growth['Pt-100_2 crucible rightside'].mean():.2f} °C\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.plot(df_growth[\"time_rel\"] / 60, df_growth[\"rogowski_300A\"])\n", + "ax.set_xlabel(\"Relative time in min\")\n", + "ax.set_ylabel(\"Heater current in A\")\n", + "\n", + "fig2, ax2 = plt.subplots()\n", + "ax2.plot(df_growth[\"time_rel\"] / 60, df_growth[\"TE_2_K crucible frontside\"], label=\"front\")\n", + "ax2.plot(df_growth[\"time_rel\"] / 60, df_growth[\"Pt-100_1 crucible backside\"], label=\"back\")\n", + "ax2.plot(df_growth[\"time_rel\"] / 60, df_growth[\"Pt-100_2 crucible rightside\"], label=\"right\")\n", + "ax2.set_xlabel(\"Relative time in min\")\n", + "ax2.set_ylabel(\"Crucible temperature in °C\")\n", + "ax2.legend()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "- Add data from other measurement devices\n", + "- Apply calibration curve to temperature sensors\n", + "- Restructure: input field at top to select sensors, time window / tag, ..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.5 ('base')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "3fe79ebdfe5b0c72d7b55e99635ccc24fad9d3af4b308f4d9c40912519481577" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/sample.py b/sample.py deleted file mode 100644 index 60a937a..0000000 --- a/sample.py +++ /dev/null @@ -1,2098 +0,0 @@ -# coding=utf8 - - -import argparse -import configparser -import DAQ_6510 -import Pyrometer -import Pyrometer_Array -import Arduino -import sys -import os -from PyQt5.QtGui import * -from PyQt5.QtCore import * -from PyQt5.QtWidgets import * -import numpy as np -import datetime -import time -import pyqtgraph as pg -from functools import partial -import random -import matplotlib - - -print ("Running sample.py ...") -parser = argparse.ArgumentParser() -parser.add_argument('-test', help='test mode without COM ports [optional, default=false]', action = 'store_true') -parser.add_argument('-time', help='test mode with timestamp from DAQ [optional, default=false]', action = 'store_true') -parser.add_argument('-cfg', help='config file name [optional, default="config.ini"]', default='config.ini') -parser.add_argument('-dt', help='sampling steps in miliseconds [optional, default=1000]', type=int, default=1000) -parser.add_argument('-nplc', help='overall NPLC for DAQ [optional]', type=float) -parser.add_argument('-lsync', help='overall line sync [optional, default=1]', type=int, default=1) -parser.add_argument('-ocom', help='overall offset compensation [optional, default=1]', type=int, default=1) -parser.add_argument('-azer', help='overall automatic zero compensation [optional, default=0]', type=int, default=0) -parser.add_argument('-adel', help='overall autodelay [optional, default=1]', type=int, default=1) -parser.add_argument('-debug_pyro', help='show the interface data [optional, default=false]', action = 'store_true') -parser.add_argument('-debug_array', help='show the interface data [optional, default=false]', action = 'store_true') -parser.add_argument('-debug_daq', help='show the interface data [optional, default=false]', action = 'store_true') -parser.add_argument('-debug_arduino', help='show the interface data [optional, default=false]', action = 'store_true') - -args = parser.parse_args() -parser.print_help() - - -# some initialisations -print ('\n') -print ('Initialising...') -print ('===============') - -if args.test: - print ('Test mode...\n') -if args.time: - print ('Timestamp mode from DAQ...\n') -if args.debug_daq: - print ('Debug mode for Keithley DAQ-6510...') -if args.debug_pyro: - print ('Debug mode for Pyrometer...') -if args.debug_array: - print ('Debug mode for Pyrometer array...') -if args.debug_arduino: - print ('Debug mode for Arduino...') - - -DAQ_present = False -DAQ_lsync = bool(args.lsync) -DAQ_ocom = bool(args.ocom) -DAQ_azer = bool(args.azer) -DAQ_adel = bool(args.adel) -Pyro_present = False -Pyro_array_present = False -Arduino_present = False -Nb_Instruments = 0 -Sampling_Timer = args.dt # in miliseconds - - - -# read the config file -ConfigFileName = args.cfg -cp = configparser.ConfigParser() -print ('reading config file: '+ ConfigFileName + '\n') -cp.read(ConfigFileName) - -ch_list = list(range(0)) # all channels, something from 101 ..120, 201..220, 501...., 601... -alias_list = list(range(0)) -sensor_list = list(range(0)) # TE,TE,PT,DCV,TE,..... -factor_list = list(range(0)) -offset_list = list(range(0)) - -daq_ch_list = list(range(0)) # only DAQ channels -daq_sensor_list = list(range(0)) -daq_alias_list = list(range(0)) -daq_range_list = list(range(0)) -daq_nplc_list = list(range(0)) -arduino_ch_list = list(range(0)) -arduino_alias_list = list(range(0)) -arduino_cmd_list = list(range(0)) -arduino_read_id_list = list(range(0)) -arduino_end_id_list = list(range(0)) -arduino_position_list = list(range(0)) -arduino_separator_list = list(range(0)) -range_list = list(range(0)) # K,K,100,100mV,J,... - -sensor_types = list(range(0)) -sensor_unit_list = list(range(0)) #[°C], [mV] - -pyro_model_list = list(range(0)) -pyro_com_list = list(range(0)) -pyro_alias_list = list(range(0)) -pyro_tr_list = list(range(0)) -pyro_em_list = list(range(0)) -pyro_rate_list = list(range(0)) -pyro_bits_list = list(range(0)) -pyro_stop_list = list(range(0)) -pyro_parity_list = list(range(0)) -pyro_t90_list = list(range(0)) -pyro_t90_times = list(range(0)) -pyro_card_list = list(range(0)) -pyro_distance_list = list(range(0)) -pyro_array_model_list = list(range(0)) -pyro_array_alias_list = list(range(0)) -pyro_array_em_list = list(range(0)) -pyro_array_t90_list = list(range(0)) -pyro_array_t90_times = list(range(0)) -pyro_array_card_list = list(range(0)) -arduino_com_list = list(range(0)) -arduino_rate_list = list(range(0)) -arduino_bits_list = list(range(0)) -arduino_stop_list = list(range(0)) -arduino_parity_list = list(range(0)) -instruments_list = list(range(0)) -arduino_card_list = list(range(0)) - - -for section_name in cp.sections(): - if section_name == 'Overflow': - for name, value in cp.items(section_name): - if name == 'daq-6510': - daq_overflow = float(value.split(',')[0].replace(' ', '')) - if name == 'pyro': - pyro_overflow = float(value.split(',')[0].replace(' ', '')) - if name == 'arduino': - arduino_overflow = float(value.split(',')[0].replace(' ', '')) - if section_name == 'PT-1000': - for name, value in cp.items(section_name): - if name == 'save': - PT_1000_mode = value - - if section_name == 'Instruments': - for name, value in cp.items(section_name): - if name == 'daq-6510': - daq_onoff = value.split(',')[0].replace(' ', '') - daq_com = value.split(',')[1].replace(' ', '') - #daq_com = int(value.split(',')[1].replace(' ', '')) - daq_rate = value.split(',')[2].replace(' ', '') - daq_bits = value.split(',')[3].replace(' ', '') - daq_stop = value.split(',')[4].replace(' ', '') - daq_parity = value.split(',')[5].replace(' ', '') - if daq_onoff == 'on': - DAQ_present = True - if name == 'pyro': - Nb_of_Pyrometer = int(value) - pyro_card_list.append('Card-05') - if Nb_of_Pyrometer != 0: - #for i in range(Nb_of_Pyrometer): - #pyro_card_list.append('Card-0' + str(5+i)) - Pyro_present = True - Nb_Instruments += 1 - instruments_list.append('Pyrometer') - if name == 'pyro-array': - pyro_array_onoff = value.split(',')[0].replace(' ', '') - #pyro_array_onoff = int(value.split(',')[0].replace(' ', '')) - pyro_array_com = int(value.split(',')[1].replace(' ', '')) - pyro_array_rate = value.split(',')[2].replace(' ', '') - pyro_array_bits = value.split(',')[3].replace(' ', '') - pyro_array_stop = value.split(',')[4].replace(' ', '') - pyro_array_parity = value.split(',')[5].replace(' ', '') - pyro_array_card_list.append('Card-20') - if pyro_array_onoff == 'on': - Pyro_array_present = True - Nb_Instruments += 1 - instruments_list.append('Pyro-array') - else: - Nb_of_pyro_array_heads = 0 - if name == 'arduino': - Nb_of_Arduino = int(value) - if Nb_of_Arduino != 0: - for i in range(Nb_of_Arduino): - arduino_card_list.append('Card-1' + str(i)) - Arduino_present = True - Nb_Instruments += 1 - instruments_list.append('Arduino') - for name, value in cp.items(section_name): - if DAQ_present: - if section_name == 'Card-01': - ch_list.append(name.replace('ch-', '1')) - daq_ch_list.append(name.replace('ch-', '1')) - daq_alias_list.append(value.split(',')[0]) - alias_list.append(value.split(',')[0]) - sensor_list.append((value.split(',')[1]).replace(' ', '')) - daq_sensor_list.append((value.split(',')[1]).replace(' ', '')) - range_list.append((value.split(',')[2]).replace(' ', '')) - daq_range_list.append((value.split(',')[2]).replace(' ', '')) - daq_nplc_list.append((value.split(',')[3]).replace(' ', '')) - factor_list.append((value.split(',')[4]).replace(' ', '')) - offset_list.append((value.split(',')[5]).replace(' ', '')) - if section_name == 'Card-02': - ch_list.append(name.replace('ch-', '2')) - daq_ch_list.append(name.replace('ch-', '2')) - daq_alias_list.append(value.split(',')[0]) - alias_list.append(value.split(',')[0]) - val = value.split(',')[1].replace(' ', '') - sensor_list.append((value.split(',')[1]).replace(' ', '')) - daq_sensor_list.append((value.split(',')[1]).replace(' ', '')) - range_list.append((value.split(',')[2]).replace(' ', '')) - daq_range_list.append((value.split(',')[2]).replace(' ', '')) - daq_nplc_list.append((value.split(',')[3]).replace(' ', '')) - factor_list.append((value.split(',')[4]).replace(' ', '')) - offset_list.append((value.split(',')[5]).replace(' ', '')) - - if Pyro_array_present: - if section_name == 'Card-20': - ch_list.append(name.replace('ch-', '20')) - pyro_array_alias_list.append(value.split(',')[0]) - alias_list.append(value.split(',')[0]) - pyro_array_em_list.append((value.split(',')[1]).replace(' ', '')) - pyro_array_t90_list.append((value.split(',')[2]).replace(' ', '')) - factor_list.append((value.split(',')[3]).replace(' ', '')) - offset_list.append((value.split(',')[4]).replace(' ', '')) - times = value[value.find('('):].replace('(', '').replace(')', '') - pyro_array_t90_times.append(times.split()) - - if Pyro_present and section_name in pyro_card_list: - if len(pyro_com_list) < Nb_of_Pyrometer: - ch_list.append(name.replace('ch-', '5')) - pyro_alias_list.append(value.split(',')[0]) - alias_list.append(value.split(',')[0]) - pyro_com_list.append((value.split(',')[1]).replace(' ', '')) - pyro_tr_list.append((value.split(',')[2]).replace(' ', '')) - pyro_em_list.append((value.split(',')[3]).replace(' ', '')) - pyro_rate_list.append((value.split(',')[4]).replace(' ', '')) - pyro_bits_list.append((value.split(',')[5]).replace(' ', '')) - pyro_stop_list.append((value.split(',')[6]).replace(' ', '')) - pyro_parity_list.append((value.split(',')[7]).replace(' ', '')) - pyro_t90_list.append((value.split(',')[8]).replace(' ', '')) - factor_list.append((value.split(',')[9]).replace(' ', '')) - offset_list.append((value.split(',')[10]).replace(' ', '')) - times = value[value.find('('):].replace('(', '').replace(')', '') - pyro_t90_times.append(times.split()) - - if Arduino_present and section_name in arduino_card_list: - if name == 'com': - arduino_com_list.append(value.split(',')[0]) - arduino_rate_list.append((value.split(',')[1]).replace(' ', '')) - arduino_bits_list.append((value.split(',')[2]).replace(' ', '')) - arduino_stop_list.append((value.split(',')[3]).replace(' ', '')) - arduino_parity_list.append((value.split(',')[4]).replace(' ', '')) - else: - ch_list.append(name.replace('ch-', section_name[5:])) - arduino_ch_list.append(name.replace('ch-', section_name[5:])) - arduino_alias_list.append(value.split(',')[0]) - alias_list.append(value.split(',')[0]) - arduino_cmd_list.append((value.split(',')[1]).replace(' ', '')) - arduino_read_id_list.append((value.split(',')[2]).replace(' ', '')) - arduino_end_id_list.append((value.split(',')[3]).replace(' ', '')) - arduino_position_list.append((value.split(',')[4]).replace(' ', '')) - arduino_separator_list.append((value.split(',')[5]).strip(' ').replace('"', '')) - factor_list.append((value.split(',')[6]).replace(' ', '')) - offset_list.append((value.split(',')[7]).replace(' ', '')) - - -Nb_all_sensors = len(ch_list) -sensor_unit_list = ['°C' for i in range(Nb_all_sensors)] -sensor_types_nb = list(range(0)) # list of integer for [#TE,#PT,#DCV,#pyro...] - -Nb_of_PlotWindows = 0 -# takes account of separate plot for arduino(s) heating status -# = Nb_of_PlotWindows - Nb_of_Additional_Arduino_Plots - -daq_sensor_types = list(range(0)) # for special parameter like LSYNC, OCOM, ... -daq_gr_list = list(range(0)) - -Nb_of_TE = sensor_list.count('TE') -if Nb_of_TE != 0: - sensor_types_nb.append(Nb_of_TE) - sensor_types.append('TE') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - daq_sensor_types.append('TE') - -Nb_of_PT = sensor_list.count('PT') -if Nb_of_PT != 0: - sensor_types_nb.append(Nb_of_PT) - sensor_types.append('PT') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - if '100' in range_list: - daq_sensor_types.append('PT-100') - if '1000' in range_list: - daq_sensor_types.append('PT-1000') -if Nb_of_TE != 0 or Nb_of_PT !=0: - Nb_Instruments += 1 - instruments_list.append('DAQ-6510-Temperatures') - -Nb_of_Rogowski = sensor_list.count('Rogowski') -if Nb_of_Rogowski != 0: - Nb_Instruments += 1 - instruments_list.append('DAQ-6510-Rogowski') - sensor_types_nb.append(Nb_of_Rogowski) - sensor_types.append('Rogowski') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - daq_sensor_types.append('Rogowski') - -Nb_of_DCV = sensor_list.count('DCV') -Nb_of_100mV = 0 -Nb_of_Volt = 0 -if Nb_of_DCV != 0: - Nb_of_100mV = range_list.count('100mV') - if Nb_of_100mV != 0: - Nb_Instruments += 1 - instruments_list.append('DAQ-6510-DCV-100mV') - sensor_types_nb.append(Nb_of_100mV) - sensor_types.append('DCV-100mV') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - daq_sensor_types.append('DCV-100mV') - Nb_of_Volt = Nb_of_DCV - Nb_of_100mV - if Nb_of_Volt != 0: - Nb_Instruments += 1 - instruments_list.append('DAQ-6510-DCV-V') - sensor_types_nb.append(Nb_of_Volt) - sensor_types.append('DCV') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - daq_sensor_types.append('DCV') - -Nb_of_ACV = sensor_list.count('ACV') -if Nb_of_ACV != 0: - Nb_Instruments += 1 - instruments_list.append('DAQ-6510-ACV') - sensor_types_nb.append(Nb_of_ACV) - sensor_types.append('ACV') - daq_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - daq_sensor_types.append('ACV') - -Nb_of_DAQ_sensors = Nb_of_TE + Nb_of_PT + Nb_of_ACV + Nb_of_DCV + Nb_of_Rogowski -Nb_of_DAQ_sensor_types = len(daq_sensor_types) - -if Pyro_present: - # add pyrometer(s) as virtual channels - pyro_gr_list = list(range(0)) - # list to plot each pyrometer in a separate plot window - # starts with the first available plot window - sensor_types_nb.append(Nb_of_Pyrometer) - sensor_types.append('Pyro') - - pyro_t90_time = list(range(0)) - for i in range(Nb_of_Pyrometer): - sensor_list.append('Pyro') - range_list.append('--') - pyro_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - p = int(pyro_t90_list[i])-1 - v = pyro_t90_times[i][p].replace(',', '') - #print (p, v) - pyro_t90_time.append(v) - -if Pyro_array_present: - # add pyrometer array as virtual channels - pyro_array_gr_list = list(range(0)) - pyro_array_t90_time = list(range(0)) - Nb_of_pyro_array_heads = len(pyro_array_alias_list) - sensor_types.append('Pyro_head') - sensor_types_nb.append(Nb_of_pyro_array_heads) - pyro_array_gr_list.append(Nb_of_PlotWindows) - Nb_of_PlotWindows += 1 - for i in range(Nb_of_pyro_array_heads): - sensor_list.append('Pyro_head') - range_list.append('--') - #pyro_array_gr_list.append(Nb_of_PlotWindows) - #Nb_of_PlotWindows += 1 - p = int(pyro_array_t90_list[i])-1 - v = pyro_array_t90_times[i][p].replace(',', '') - #print (p, v) - pyro_array_t90_time.append(v) - - -if Arduino_present: - # add arduino(s) as virtual channels - arduino_gr_list = list(range(0)) - # list to plot each arduino in a separate plot window - # starts with the first available plot window - sensor_types_nb.append(Nb_of_Arduino) - sensor_types.append('Arduino') - arduino_last_channel = arduino_ch_list[0] - arduino_heating_command = 'h' - for j in range(len(arduino_ch_list)): - arduino_active_channel = arduino_ch_list[j] - sensor_list.append('Arduino') - range_list.append('--') - if arduino_cmd_list[j] == arduino_heating_command or arduino_active_channel[0:2] != arduino_last_channel[0:2]: - Nb_of_PlotWindows += 1 - arduino_gr_list.append(Nb_of_PlotWindows) - arduino_last_channel = arduino_active_channel - Nb_of_PlotWindows += 1 - Nb_of_Additional_Arduino_Plots = arduino_cmd_list.count(arduino_heating_command) - - -for i in range(Nb_all_sensors): - if sensor_list[i] == 'Rogowski': - sensor_unit_list[i] = 'V' - if sensor_list[i] == 'DCV' and range_list[i] != '100mV': - sensor_unit_list[i] = 'V' - if sensor_list[i] == 'DCV' and range_list[i] == '100mV': - sensor_list[i] = 'DCV-100mV' - #sensor_unit_list[i] = 'mV' - sensor_unit_list[i] = 'V' -for i in range(len(daq_sensor_list)): - if daq_sensor_list[i] == 'DCV' and daq_range_list[i] == '100mV': - daq_sensor_list[i] = 'DCV-100mV' - -print ('number of instruments: ', Nb_Instruments) -print ('Instruments: ', instruments_list) -print ('Number of all sensors: ', Nb_all_sensors) -print ('Active channel list: ', ch_list) -print ('Active sensor list: ', sensor_list) -print ('Active sensor range list:', range_list) -print ('Sensor measurements unit: ', sensor_unit_list) -print ('Sensor alias list: ', alias_list) -print ('Sensor types: ', sensor_types) -Nb_sensor_types = len(sensor_types_nb) -print ('Number of sensor types: ', Nb_sensor_types) -print ('sensor types number: ', sensor_types_nb) -print ('Channel * factors: ', factor_list) -print ('Channel offsets: ', offset_list) - - -if DAQ_present: - print ('\n===============================') - print ('Setting up Keithley DAQ-6510...') - print ('===============================') - print ('Keithley DAQ-6510 @ COM -', daq_com, ' : ', daq_rate, ', ', daq_bits, ', ', daq_stop, ', ', daq_parity) - print ('DAQ overflow value: ', daq_overflow) - print ('DAQ alias list: ', daq_alias_list) - print ('DAQ sensor types: ', daq_sensor_types) - print ('number of DAQ sensors: ', Nb_of_DAQ_sensors) - print ('DAQ graphics list: ', daq_gr_list) - - if args.nplc is not None: - print ('Setting overall NPLC to: ', args.nplc) - for i in range(len(daq_nplc_list)): - daq_nplc_list[i] = str(args.nplc) - print ('DAQ NPLC(s): ', daq_nplc_list) - print ('Overall LSYNC is: ', DAQ_lsync) - print ('Overall OCOM is: ', DAQ_ocom) - print ('Overall AZER is: ', DAQ_azer) - print ('Overall ADEL is: ', DAQ_adel) - - - DAQ_6510.init_daq(daq_com, daq_rate, daq_bits, daq_stop, daq_parity, args.debug_daq, args.test, args.time) - DAQ_6510.reset_daq() - DAQ_6510.idn_daq() - DAQ_6510.idn_card_1() - DAQ_6510.idn_card_2() - DAQ_6510.config_daq(daq_ch_list, daq_alias_list, daq_sensor_list, daq_range_list, daq_sensor_types, daq_nplc_list, \ - PT_1000_mode, DAQ_lsync, DAQ_ocom, DAQ_azer, DAQ_adel) - - -if Pyro_present: - print ('\n==========================') - print ('Setting up pyrometer(s)...') - print ('==========================') - print ('Pyro overflow value: ', pyro_overflow) - print ('Pyro com ports: ', pyro_com_list) - print ('Pyro alias list: ', pyro_alias_list) - print ('Pyro transmissions: ', pyro_tr_list) - print ('Pyro emissions: ', pyro_em_list) - print ('Pyro datarate: ', pyro_rate_list) - print ('Pyro bits: ', pyro_bits_list) - print ('Pyro stop: ', pyro_stop_list) - print ('Pyro parity: ', pyro_parity_list) - print ('Pyro t90: ', pyro_t90_list, pyro_t90_time) - print ('Pyro card(s) list: ', pyro_card_list) - print ('Pyro graphics list: ', pyro_gr_list) - - for i in range(Nb_of_Pyrometer): - print ('Init Pyro ', i+1) - Pyrometer.Init_Pyro(i, pyro_com_list[i], pyro_rate_list[i], pyro_bits_list[i], \ - pyro_stop_list[i], pyro_parity_list[i], args.debug_pyro, args.test) - print ('Config Pyro ', i+1) - Pyrometer.Config_Pyro(i, pyro_em_list[i], pyro_tr_list[i]) - if not args.test: - pyro_model_list.append(Pyrometer.Get_ID(i)) - print ('distance=') - print (str(Pyrometer.Get_Focus(i))[-4:]) - focus = int(str(Pyrometer.Get_Focus(i))[-4:]) - pyro_distance_list.append(str(focus)) - else: - focus_str = '02000390' - focus = int(focus_str[-4:]) - pyro_model_list.append('xxx') - pyro_distance_list.append(str(focus)) - - #print (str(pyro_t90_times[i]).replace('[','').replace(']','').replace(',','').replace("'",'').split()) - #print (len(str(pyro_t90_times[i]).replace('[','').replace(']','').replace(',','').replace("'",'').split())) - - print ('Pyro model list: ', pyro_model_list) - - -if Pyro_array_present: - print ('\n==========================') - print ('Setting up pyrometer array...') - print ('==========================') - print ('Pyro array overflow value: ', pyro_overflow) - print ('Pyro array COM port: ', pyro_array_com) - print ('Pyro array datarate: ', pyro_array_rate) - print ('Pyro array bits: ', pyro_array_bits) - print ('Pyro array stop: ', pyro_array_stop) - print ('Pyro array parity: ', pyro_array_parity) - print ('Pyro array alias list: ', pyro_array_alias_list) - print ('Pyro array emissions: ', pyro_array_em_list) - print ('Pyro array t90: ', pyro_array_t90_list, pyro_array_t90_time) - print ('Pyro array array card: ', pyro_array_card_list) - print ('Pyro array graphics list: ', pyro_array_gr_list) - - print ('Init Pyro Array') - Pyrometer_Array.Init_Pyro_Array(pyro_array_com, pyro_array_rate, pyro_array_bits, pyro_array_stop, \ - pyro_array_parity, args.debug_array, args.test) - print ('Config Pyro Array') - for i in range(Nb_of_pyro_array_heads): - if not args.test: - Pyrometer_Array.Config_Pyro_Array(i, pyro_array_em_list[i]) - pyro_array_model_list.append(Pyrometer_Array.Get_head_ID(i)) - else: - pyro_array_model_list.append('xxx') - - print ('Pyro Array model list: ', pyro_array_model_list) - -if Arduino_present: - print ('\n==========================') - print ('Setting up Arduino(s)...') - print ('==========================') - print ('Arduino overflow value: ', arduino_overflow) - print ('Arduino com port(s): ', arduino_com_list) - print ('Arduino rate(s): ', arduino_rate_list) - print ('Arduino bit(s): ', arduino_bits_list) - print ('Arduino stop(s): ', arduino_stop_list) - print ('Arduino parity(s): ', arduino_parity_list) - print ('Arduino card(s) list: ', arduino_card_list) - print ('Arduino channel list: ', arduino_ch_list) - print ('Arduino alias list: ', arduino_alias_list) - print ('Arduino command list: ', arduino_cmd_list) - print ('Arduino read-id list: ', arduino_read_id_list) - print ('Arduino end-id list: ', arduino_end_id_list) - print ('Arduino position list: ', arduino_position_list) - print ('Arduino separator list: ', arduino_separator_list) - print ('Arduino graphics list: ', arduino_gr_list) - - for i in range(Nb_of_Arduino): - print ('Init Arduino: ', i+1) - Arduino.Init_Arduino(i, arduino_com_list[i], arduino_rate_list[i], arduino_bits_list[i],\ - arduino_stop_list[i], arduino_parity_list[i], args.debug_arduino, args.test) - - arduino_first_channel = arduino_ch_list[0] - -# create the graphics index, means the correct graphics window for each sensor or sensor type -# sensor type heating status from arduino needs also extra window -gr_idx = list(range(Nb_all_sensors)) -for i in range(Nb_sensor_types): - z = 0 - z1 = 0 - z2 = 0 - for j in range(Nb_all_sensors): - if sensor_list[j] == sensor_types[i]: - gr_idx[j] = i - if sensor_list[j] == 'Pyro': - gr_idx[j] = pyro_gr_list[z] - z += 1 - if sensor_list[j] == 'Pyro_head': - gr_idx[j] = pyro_array_gr_list[z1] - #z1 += 1 - if sensor_list[j] == 'Arduino': - gr_idx[j] = arduino_gr_list[z2] - z2 += 1 - - -# and begin the colors list for each new graphics window again -color_list = list(range(Nb_all_sensors)) -for z in range(Nb_of_PlotWindows): - indices = [i for i, x in enumerate(gr_idx) if x == z] - k = 0 - for j in range(len(indices)): - color_list[indices[j]] = k - k += 1 - -print ('\n \n') -print ('number of PlotWindows: ', Nb_of_PlotWindows) -if Arduino_present: - print ('number of additional Arduino plots: ', Nb_of_Additional_Arduino_Plots) - -print ('graphics index: ', gr_idx) -print ('colors list: ', color_list) -print ('\n') - -#=========================================== -def Init_Output_File(ch): -#=========================================== -# Init data file output -# Init online protocol - - global FileOutName, ProtocolFileName - - actual_date = datetime.datetime.now().strftime('%Y_%m_%d') - FileOutPrefix = actual_date - FileOutIndex = str(1).zfill(2) - FileOutName = '' - - FileOutName = FileOutPrefix + '_#' + FileOutIndex + '.dat' - ProtocolFileName = FileOutPrefix + '_#' + FileOutIndex + '_op.txt' - j = 1 - while os.path.exists(FileOutName) : - j = j + 1 - FileOutIndex = str(j).zfill(2) - FileOutName = FileOutPrefix + '_#' + FileOutIndex + '.dat' - ProtocolFileName = FileOutPrefix + '_#' + FileOutIndex + '_op.txt' - print ('Output data: ', FileOutName) - print ('Online Protocol: ', ProtocolFileName) - OutputFile = open(FileOutName, 'w') - # write sensor list - OutputFile.write('Sensor list\n') - OutputFile.write(str(ch_list) + '\n') - OutputFile.write('Alias list\n') - OutputFile.write(str(alias_list) + '\n') - OutputFile.write(str(sensor_list) + '\n') - OutputFile.write(str(range_list) + '\n') - OutputFile.write('--------------------------------\n') - OutputFile.write('time') - OutputFile.write('time'.rjust(16)) - for j in range(Nb_all_sensors): - OutputFile.write((ch[j]).rjust(14)) - if sensor_list[j] == 'PT' and range_list[j] == '1000' and PT_1000_mode == 'R+T': - OutputFile.write((ch[j]).rjust(14)) - OutputFile.write('\n') - OutputFile.write('abs.') - OutputFile.write('s'.rjust(16)) - for j in range(Nb_all_sensors): - if sensor_list[j] == 'PT' and range_list[j] == '1000' and PT_1000_mode == 'R+T': - OutputFile.write('°C'.rjust(14)) - OutputFile.write('Ohm'.rjust(14)) - elif sensor_list[j] == 'PT' and range_list[j] == '1000' and PT_1000_mode == 'R': - OutputFile.write('Ohm'.rjust(14)) - elif sensor_list[j] == 'Rogowski': - OutputFile.write('A'.rjust(14)) - else: - OutputFile.write(sensor_unit_list[j].rjust(14)) - - OutputFile.write('\n') - OutputFile.close( ) - - ProtocolFile = open(ProtocolFileName, 'w') - ProtocolFile.write('Online protocol for: ' + FileOutName + '\n') - ProtocolFile.write('=========================================\n') - - ProtocolFile.write('Sampling with: dt[ms]=' + str(Sampling_Timer) + '\n') - if DAQ_present: - ProtocolFile.write('\nStarting values for DAQ:\n') - ProtocolFile.write('LSYNC for all DAQ-channels: ' + str(DAQ_lsync) + '\n') - ProtocolFile.write('OCOM for all DAQ-channels: ' + str(DAQ_ocom) + '\n') - ProtocolFile.write('AZER for all DAQ-channels: ' + str(DAQ_azer) + '\n') - ProtocolFile.write('ADEL for all DAQ-channels: ' + str(DAQ_adel) + '\n') - - for i in range(Nb_of_DAQ_sensors): - ProtocolFile.write(daq_alias_list[i] + ' : ' + 'NPLC=' + daq_nplc_list[i] + ', ' \ - + 'sensor=' + daq_sensor_list[i] + ', ' + 'type=' + daq_range_list[i] +'\n') - - if Pyro_present: - ProtocolFile.write('\nStarting values for pyrometer:\n') - for i in range(Nb_of_Pyrometer): - ProtocolFile.write('Pyrometer ' + str(i+1) + ' : ' + pyro_alias_list[i] + ', ' + pyro_model_list[i] + ', ' + 'emission=' + pyro_em_list[i] + '%' + ', ' + 'transmission=' + pyro_tr_list[i] + '%' \ - + ', ' + 't90=' + pyro_t90_time[i] + 's' + ', ' + 'distance=' + pyro_distance_list[i] + 'mm' + '\n') - - if Pyro_array_present: - ProtocolFile.write('\nStarting values for pyrometer-array:\n') - for i in range(Nb_of_pyro_array_heads): - ProtocolFile.write('Head ' + str(i+1) + ' : ' + pyro_array_alias_list[i] + ', ' + pyro_array_model_list[i] \ - + ', ' + 'emission=' + pyro_array_em_list[i] + '%' \ - + ', ' + 't90=' + pyro_array_t90_time[i] + 's' + '\n') - - - - ProtocolFile.write('\nstarting with measurements....') - ProtocolFile.write('\n----------------------------\n') - - ProtocolFile.close() - -#=========================================== -def calc_temp_PT1000(r): -#=========================================== - a = 3.9083E-3 - b = -0.5775E-6 - d = a**2 - 4*b*(1-r/1000) - temp = (-a+np.sqrt(d))/2/b - return (temp) - - -#=========================================== -def get_measurements(): -#=========================================== -# Get the sampled data from the active instrument(s) - data = list(range(0)) - data_ohm = list(range(0)) # ohm values from PT-1000 - - if DAQ_present: - # comma separated string: channel,reading,channel,reading, ..... - m = DAQ_6510.get_daq() - if args.time: - print (m,'\n') - m_list = m.split(',') - if args.time: - step = 3 # with timestamp - else: - step = 2 # without timestamp - for i in range(0, len(m_list)-1, step): - if m_list[i] != daq_ch_list[int(i/step)]: - print ('Error in Keithley channels....') - if sensor_list[int(i/step)] == 'PT' and range_list[int(i/step)] == '1000': - # 'R': ohm value - # 'T': temp value calculated by the DAQ - # 'R+T': ohm value, must be calculated by the own function - if PT_1000_mode == 'R': - val_R = float(m_list[i+step-1]) - if val_R >= daq_overflow: - print ('Overflow from DAQ') - val_R = np.nan - data.append(val_R) - if PT_1000_mode == 'R+T': - val_R = float(m_list[i+step-1]) - val_T = round(calc_temp_PT1000(val_R), 3) - if val_R >= daq_overflow: - print ('Overflow from DAQ') - val_R = np.nan - val_T = np.nan - data.append(val_T) - data_ohm.append(val_R) - if PT_1000_mode == 'T': - val_T = float(m_list[i+step-1]) - if val_T >= daq_overflow: - print ('Overflow from DAQ') - val_T = np.nan - data.append(val_T) - else: - val = round(float(m_list[i+step-1]), 6) - if val >= daq_overflow: - print ('Overflow from DAQ') - val = np.nan - data.append(val) - - if Pyro_present: - for i in range(Nb_of_Pyrometer): - val = Pyrometer.Read_Pyro(i) - val = round(val, 3) - if val >= pyro_overflow: - print ('Overflow from Pyrometer number ', i+1) - val = np.nan - data.append(val) - - if Pyro_array_present: - for i in range(Nb_of_pyro_array_heads): - val = Pyrometer_Array.Read_Pyro_Array(i) - val = round(val, 3) - if val >= pyro_overflow: - print ('Overflow from Pyrometer-Array head number ', i+1) - val = np.nan - data.append(val) - - if Arduino_present: - nb1 = 0 - for i in range(Nb_of_Arduino): - nb2 = nb1 - for j in range(len(arduino_ch_list)): - if arduino_card_list[i][5:] == arduino_ch_list[j][:2]: - nb2 += 1 - result = Arduino.Read_Arduino(i, arduino_cmd_list[nb1:nb2], arduino_read_id_list[nb1:nb2], \ - arduino_end_id_list[nb1:nb2], arduino_position_list[nb1:nb2], \ - arduino_separator_list[nb1:nb2]) - nb1 = nb2 - val = round(val,3) - for z, val in enumerate(result): - if val >= arduino_overflow: - print ('Overflow from Arduino number ', i+1) - val = np.nan - data.append(val) - return [data, data_ohm] - - -def Graphics(): - # ==================================================== - # main loop of the GUI - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas - - global time_start, sampling_started - time_start = datetime.datetime.now() - sampling_started = False - - # scaling initialisations - x_min = 0 # bottom x-axis scaling - x_max = 60 - y_min = 0 # temperature scaling - y_max = 20 - x2_list = [] - x2_ticks = [] # init ticks - x2_Nb_ticks = 4 # show everytime ? ticks at top x axis - delta_t = int(x_max - x_min) - - for i in range(x2_Nb_ticks): - x2_list.append((datetime.datetime.now() + datetime.timedelta(seconds=i*delta_t/(x2_Nb_ticks-1))).strftime('%H:%M:%S')) - x2_ticks.append([i*delta_t/(x2_Nb_ticks-1), x2_list[i]]) - - tab_list = list(range(0)) - tab_daq_TE_PT = 'Temperatures' - tab_daq_Rogowski = 'Rogowski' - tab_daq_100mV = 'DCV 100mV' - tab_daq_Volt = 'DCV Volt' - tab_daq_ACV = 'ACV Volt' - tab_pyro = 'Pyrometer(s)' - tab_pyro_array = 'Pyro-array' - tab_arduino = 'Arduino(s)' - tab_misc_para = 'other parameter' - - if DAQ_present: - if Nb_of_TE != 0 or Nb_of_PT !=0: - tab_list.append(tab_daq_TE_PT) - if Nb_of_Rogowski != 0: - tab_list.append(tab_daq_Rogowski) - if Nb_of_100mV != 0: - tab_list.append(tab_daq_100mV) - if Nb_of_Volt != 0: - tab_list.append(tab_daq_Volt) - if Nb_of_ACV != 0: - tab_list.append(tab_daq_ACV) - if Pyro_present: - tab_list.append(tab_pyro) - if Pyro_array_present: - tab_list.append(tab_pyro_array) - if Arduino_present: - tab_list.append(tab_arduino) - #tab_list.append(tab_misc_para) - - print ('Number of tabs: ', len(tab_list)) - - class grPanel(QWidget): - pdg = 0.0 - - def __init__(self, parent=None): - - class QHLine(QFrame): - def __init__(self): - super(QHLine, self).__init__() - self.setFrameShape(QFrame.HLine) - self.setFrameShadow(QFrame.Sunken) - - class LineEdit(QLineEdit): - def __init__(self, parent = None): - super(LineEdit, self).__init__(parent) - def focusInEvent(self, e): - super(LineEdit, self).focusInEvent(e) - self.setStyleSheet('color: red') - self.selectAll() - def mousePressEvent(self, e): - self.setStyleSheet('color: red') - self.selectAll() - #def focusOutEvent(self, e): - #super(LineEdit, self).focusOutEvent(e) - #self.deselect() - #self.setStyleSheet('color: black') - - super(grPanel, self).__init__(parent) - - self.myPen = list(range(20)) - self.mycolors = ['red','green','cyan','magenta','blue','orange','darkmagenta','yellow','turquoise','purple','brown','tomato','lime','olive','navy','darkmagenta','beige','peru','grey','white'] - - for i in range(20): - self.myPen[i] = pg.mkPen(color=matplotlib.colors.cnames[self.mycolors[i]]) - #self.myPen[i] = pg.mkPen(color=(i,20)) - - - # each row is a sampled time step - self.pData_time = np.zeros((0, 2)) # Dim-0 = time/abs, Dim-1=time/s - self.pData_temp = np.empty((0, Nb_all_sensors)) # following Dim's are temperatures - - # graphics for TE, PT, DCV, .... - #self.canvas = [myGraphics() for i in range(Nb_Instruments)] - #self.canvas = [pg.GraphicsLayoutWidget() for i in range(Nb_Instruments)] - self.canvas = [pg.GraphicsWindow() for i in range(Nb_Instruments)] - self.gr = list(range(Nb_of_PlotWindows)) - self.pData_line = list(range(Nb_all_sensors)) - self.ax_X = list(range(Nb_of_PlotWindows)) # axis object from the plot (bottom) - self.ax_X_2 = list(range(Nb_of_PlotWindows)) # axis object from the plot (top) - self.ax_Y = list(range(Nb_of_PlotWindows)) # axis object from the plot (left) - - for i in range(Nb_sensor_types): - if sensor_types[i] == 'TE' or sensor_types[i] == 'PT':# or sensor_types[i]== 'DCV' or sensor_types[i] == 'ACV': - tab_idx = tab_list.index(tab_daq_TE_PT) - if sensor_types[i] == 'Rogowski': - tab_idx = tab_list.index(tab_daq_Rogowski) - if sensor_types[i] == 'DCV-100mV': - tab_idx = tab_list.index(tab_daq_100mV) - if sensor_types[i] == 'DCV': - tab_idx = tab_list.index(tab_daq_Volt) - if sensor_types[i] == 'ACV': - tab_idx = tab_list.index(tab_daq_ACV) - if sensor_types[i] == 'Pyro': - tab_idx = tab_list.index(tab_pyro) - if sensor_types[i] == 'Pyro_head': - tab_idx = tab_list.index(tab_pyro_array) - if sensor_types[i] == 'Arduino': - tab_idx = tab_list.index(tab_arduino) - - # create each sensor type in a separate plot window - if sensor_types[i] == 'Pyro': - # for more than one pyrometer each one in a seperate plot - for j in range(Nb_of_Pyrometer): - idx = pyro_gr_list[j] - self.gr[idx] = self.canvas[tab_idx].addPlot(j, 0) - self.gr[idx].setXRange(x_min, x_max, padding=self.pdg) - self.gr[idx].setYRange(y_min, y_max, padding=self.pdg) - self.gr[idx].setLabel('left', 'temp [°C]', color='red') - self.gr[idx].setLabel('bottom', 'time [s]') - self.gr[idx].setLabel('top') - self.gr[idx].setLabel('right', '') - self.gr[idx].showGrid(x=True, y=True) - self.ax_X[idx] = self.gr[idx].getAxis('bottom') - self.ax_Y[idx] = self.gr[idx].getAxis('left') - self.ax_X_2[idx] = self.gr[idx].getAxis('top') - self.ax_X_2[idx].setTicks([x2_ticks,[]]) - else: - if sensor_types[i] == 'Pyro_head': - for j in range(len(pyro_array_gr_list)): - idx = pyro_array_gr_list[j] - self.gr[idx] = self.canvas[tab_idx].addPlot(j, 0) - self.gr[idx].setXRange(x_min, x_max, padding=self.pdg) - self.gr[idx].setYRange(y_min, y_max, padding=self.pdg) - self.gr[idx].setLabel('left', 'temp [°C]', color='red') - self.gr[idx].setLabel('bottom', 'time [s]') - self.gr[idx].setLabel('top') - self.gr[idx].setLabel('right', '') - self.gr[idx].showGrid(x=True, y=True) - self.ax_X[idx] = self.gr[idx].getAxis('bottom') - self.ax_Y[idx] = self.gr[idx].getAxis('left') - self.ax_X_2[idx] = self.gr[idx].getAxis('top') - self.ax_X_2[idx].setTicks([x2_ticks,[]]) - else: - if sensor_types[i] == 'Arduino': - # for more than one arduino each one in a seperate plot - # arduino with sensor type heat status also in a separate plot - previous_index = arduino_gr_list[0] - for j in range(len(arduino_gr_list)): - idx = arduino_gr_list[j] - if j == 0 or idx != previous_index: - self.gr[idx] = self.canvas[tab_idx].addPlot(j, 0) - self.gr[idx].setMouseEnabled(x = False, y = False) - self.gr[idx].setXRange(x_min, x_max, padding=self.pdg) - self.gr[idx].setYRange(y_min, y_max, padding=self.pdg) - if arduino_cmd_list[j] == arduino_heating_command: - self.gr[idx].setLabel('left', 'heating ON / OFF', color='green') - else: - self.gr[idx].setLabel('left', 'temp [°C]', color='red') - self.gr[idx].setLabel('bottom', 'time [s]') - self.gr[idx].setLabel('top') - self.gr[idx].setLabel('right', '') - self.gr[idx].showGrid(x=True, y=True) - self.ax_X[idx] = self.gr[idx].getAxis('bottom') - self.ax_Y[idx] = self.gr[idx].getAxis('left') - self.ax_X_2[idx] = self.gr[idx].getAxis('top') - self.ax_X_2[idx].setTicks([x2_ticks,[]]) - previous_index = idx - else: - self.gr[i] = self.canvas[tab_idx].addPlot(i, 0) - self.gr[i].setXRange(x_min, x_max, padding=self.pdg) - self.gr[i].setYRange(y_min, y_max, padding=self.pdg) - self.gr[i].setLabel('left', 'temp [°C]', color='red') - if sensor_types[i] == 'Rogowski': - self.gr[i].setLabel('left', 'current [A]', color='red') - if sensor_types[i] == 'DCV-100mV': - self.gr[i].setLabel('left', 'voltage [V]', color='red') - if sensor_types[i] == 'DCV': - self.gr[i].setLabel('left', 'voltage [V]', color='red') - if sensor_types[i] == 'ACV': - self.gr[i].setLabel('left', 'voltage [V]', color='red') - self.gr[i].setLabel('bottom', 'time [s]') - self.gr[i].setLabel('top') - self.gr[i].setLabel('right', '') - self.gr[i].showGrid(x=True, y=True) - self.ax_X[i] = self.gr[i].getAxis('bottom') - self.ax_Y[i] = self.gr[i].getAxis('left') - self.ax_X_2[i] = self.gr[i].getAxis('top') - self.ax_X_2[i].setTicks([x2_ticks,[]]) - - x = self.pData_time[:,0] - for i in range(Nb_all_sensors): - # begin the colors list for each new graphics window again - y = self.pData_temp[:, gr_idx[i]] - self.pData_line[i] = self.gr[gr_idx[i]].plot(x, y, pen=self.myPen[color_list[i]]) - - - # init timer for the sampling - self.timer = QTimer() - self.timer.setInterval(Sampling_Timer) - self.timer.timeout.connect(self.update_graphics) - - self.btn_Start = QPushButton() - self.btn_Start.setText('Start') - self.btn_Start.setMaximumWidth(300) - self.btn_Start.setIcon(QIcon('Start-icon.png')) - self.btn_Start.setFont(QFont('Times', 16, QFont.Bold)) - self.btn_Start.setStyleSheet('color: red') - self.btn_Start.setEnabled(True) - - self.btn_Pause = QPushButton() - self.btn_Pause.setText('Pause') - self.btn_Pause.setMaximumWidth(300) - self.btn_Pause.setIcon(QIcon('Pause-icon.png')) - self.btn_Pause.setFont(QFont('Times', 16, QFont.Bold)) - self.btn_Pause.setStyleSheet('color: red') - self.btn_Pause.setEnabled(False) - - self.btn_Exit = QPushButton() - self.btn_Exit.setText('Exit') - self.btn_Exit.setMaximumWidth(380) - self.btn_Exit.setIcon(QIcon('Exit-icon.png')) - self.btn_Exit.setFont(QFont('Times', 16, QFont.Bold)) - self.btn_Exit.setStyleSheet('color: red') - self.btn_Exit.setEnabled(True) - - self.lbl_1_current_time = QLabel('time: ') - self.lbl_1_current_time.setFont(QFont('Times', 14)) - self.lbl_1_current_time.setStyleSheet('color: black') - self.lbl_2_current_time = QLabel('xx') - self.lbl_2_current_time.setFont(QFont('Times', 14, QFont.Bold)) - self.lbl_2_current_time.setStyleSheet('color: blue') - self.lbl_1_start_sampling_time = QLabel('started : ') - self.lbl_1_start_sampling_time.setFont(QFont('Times', 14)) - self.lbl_1_start_sampling_time.setStyleSheet('color: black') - self.lbl_2_start_sampling_time = QLabel('xx') - self.lbl_2_start_sampling_time.setFont(QFont('Times', 14)) - self.lbl_2_start_sampling_time.setStyleSheet('color: red') - self.lbl_file_name = QLabel('filename: ') - self.lbl_file_name.setFont(QFont('Times', 14)) - self.lbl_file_name.setStyleSheet('color: black') - self.file_name = QLabel('xxx') - self.file_name.setFont(QFont('Times', 14)) - self.file_name.setStyleSheet('color: red') - - MainLayout = QVBoxLayout() - self.setLayout(MainLayout) - - ButtonLayout = QHBoxLayout() - GraphicsLayout = [QVBoxLayout() for i in range(Nb_Instruments)] - ParameterLayout = [QVBoxLayout() for i in range(Nb_Instruments)] - ParameterGroupLayout = list(range(Nb_of_PlotWindows)) - for i in range(Nb_of_PlotWindows): - # layout inside the specific sensor type (TE, PT, DCV, ...) - ParameterGroupLayout[i] = QGridLayout() - - Graphics = [QWidget() for i in range(Nb_Instruments)] - for i in range(Nb_Instruments): - Graphics[i].setLayout(GraphicsLayout[i]) - - Parameter = [QWidget() for i in range(Nb_Instruments)] - for i in range(Nb_Instruments): - Parameter[i].setLayout(ParameterLayout[i]) - Parameter_Group = list(range(Nb_of_PlotWindows)) - for i in range(Nb_of_PlotWindows): - idx = gr_idx.index(i) - Parameter_Group[i] = QGroupBox(sensor_list[idx]) - if sensor_list[idx] == 'Arduino': - group_name = 'Arduino - ' + str(int(ch_list[idx][0:2])-9) - Parameter_Group[i] = QGroupBox(group_name) - if sensor_list[idx] == 'Pyro': - group_name = alias_list[idx] - Parameter_Group[i] = QGroupBox(group_name) - if sensor_list[idx] == 'Pyro_head': - group_name = 'Pyro-array' #alias_list[idx] - Parameter_Group[i] = QGroupBox(group_name) - Parameter_Group[i].setObjectName('Group') - Parameter_Group[i].setStyleSheet('QGroupBox#Group{border: 1px solid black; color: black; \ - font-size: 16px; subcontrol-position: top left; font-weight: bold;\ - subcontrol-origin: margin; padding: 10px}') - for i in range(Nb_of_PlotWindows): - Parameter_Group[i].setLayout(ParameterGroupLayout[i]) - - Button = QWidget() - Button.setLayout(ButtonLayout) - - SplitterStylesheet = "QSplitter::handle{background: LightGrey; width: 5px; height: 5px;}" - Splitter_Display = QSplitter(Qt.Vertical,frameShape=QFrame.StyledPanel) ## trennt die Hauptbereiche wie Graphics+Parameter und HauptButtons - Splitter_Display.setChildrenCollapsible(False) - Splitter_Display.setStyleSheet(SplitterStylesheet) - - Splitter_Para_Display = [QSplitter(Qt.Horizontal,frameShape=QFrame.StyledPanel) for i in range(Nb_Instruments)] - for i in range(Nb_Instruments): - Splitter_Para_Display[i].setChildrenCollapsible(True) - Splitter_Para_Display[i].setStyleSheet(SplitterStylesheet) - - tabs = QTabWidget() - tabs.setStyleSheet('QTabBar {font-size: 14pt; color: blue;}') - tab = Splitter_Para_Display - - scroll = QScrollArea() - scroll.setWidget(tabs) - scroll.setWidgetResizable(True) - screen_width = gr_app.desktop().screenGeometry().width() - screen_height = gr_app.desktop().screenGeometry().height() - if screen_width == 1280: - scroll.setFixedHeight(850) - else: - scroll.setFixedHeight(1000) - - MainLayout.addWidget(Splitter_Display) - Splitter_Display.addWidget(tabs)#(scroll)#(tabs) - group = 0 - for i in range(Nb_Instruments): - tabs.addTab(tab[i], tab_list[i]) - tab[i].addWidget(Graphics[i]) - tab[i].addWidget(Parameter[i]) - GraphicsLayout[i].addWidget(self.canvas[i]) - if tab_list[i] == tab_daq_TE_PT: - if 'TE' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if 'PT' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_daq_Rogowski: - if 'Rogowski' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_daq_100mV: - if 'DCV-100mV' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_daq_Volt: - if 'DCV' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_daq_ACV: - if 'ACV' in sensor_types: - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_pyro: - for j in range(Nb_of_Pyrometer): - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_pyro_array: - #for j in range(Nb_of_pyro_array_heads): - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - if tab_list[i] == tab_arduino: - for j in range(Nb_of_Arduino + Nb_of_Additional_Arduino_Plots): - ParameterLayout[i].addWidget(Parameter_Group[group]) - group += 1 - - - various_parameter_Layout = QGridLayout() - various_parameter = QWidget() - various_parameter.setLayout(various_parameter_Layout) - #t_i = tab_list.index(tab_misc_para) - #tabs.addTab(various_parameter, tab_list[t_i]) - - self.edit_dt = LineEdit() - self.edit_dt.setFixedWidth(80) - self.edit_dt.setFont(QFont('Times', 14, QFont.Bold)) - self.edit_dt.setText(str(Sampling_Timer)) - self.lbl_edit_dt = QLabel('sampling dt[ms] : ') - self.lbl_edit_dt.setFont(QFont('Times', 14)) - - self.edit_dt.returnPressed.connect(self.edit_dt_changed) - #self.edit_dt.editingFinished.connect(self.edit_dt_changed) - - Splitter_Display.addWidget(Button) - - ButtonLayout.addWidget(self.btn_Start) - ButtonLayout.addWidget(self.btn_Pause) - ButtonLayout.addWidget(self.btn_Exit) - ButtonLayout.setSpacing(20) - self.btn_Start.clicked.connect(self.btn_Start_click) - self.btn_Exit.clicked.connect(self.btn_Exit_click) - self.btn_Pause.clicked.connect(self.btn_Pause_click) - #ButtonLayout.addStretch(1) - ButtonLayout.addWidget(self.lbl_file_name, Qt.AlignRight) - ButtonLayout.addWidget(self.file_name, Qt.AlignLeft) - #ButtonLayout.addStretch(1) - ButtonLayout.addWidget(self.lbl_1_current_time, Qt.AlignRight) - ButtonLayout.addWidget(self.lbl_2_current_time, Qt.AlignLeft) - #ButtonLayout.addStretch(1) - ButtonLayout.addWidget(self.lbl_1_start_sampling_time, Qt.AlignRight) - ButtonLayout.addWidget(self.lbl_2_start_sampling_time, Qt.AlignLeft) - #ButtonLayout.addStretch(1) - ButtonLayout.addWidget(self.lbl_edit_dt, Qt.AlignRight) - ButtonLayout.addWidget(self.edit_dt, Qt.AlignLeft) - - self.spacer = QSpacerItem(20,20) - - self.x_min = list(range(Nb_of_PlotWindows)) #scale x-axis - self.x_max = list(range(Nb_of_PlotWindows)) - self.y_min = list(range(Nb_of_PlotWindows)) #scale y-axis - self.y_max = list(range(Nb_of_PlotWindows)) - self.lbl_x_edit = list(range(Nb_of_PlotWindows)) - self.lbl_y_edit = list(range(Nb_of_PlotWindows)) - self.edit_x_min = list(range(Nb_of_PlotWindows)) - self.edit_x_max = list(range(Nb_of_PlotWindows)) - self.edit_y_min = list(range(Nb_of_PlotWindows)) - self.edit_y_max = list(range(Nb_of_PlotWindows)) - self.sensor_value = list(range(Nb_all_sensors)) # actual sampled value - self.lbl_sensor_name = list(range(Nb_all_sensors)) - self.ch_AutoScale_x = list(range(Nb_of_PlotWindows)) - self.ch_AutoScale_y = list(range(Nb_of_PlotWindows)) - self.ch_sensor_value = list(range(Nb_all_sensors)) - self.edit_Pyro_em = list(range(Nb_of_Pyrometer)) - self.edit_Pyro_tr = list(range(Nb_of_Pyrometer)) - self.lbl_edit_Pyro_em = list(range(Nb_of_Pyrometer)) - self.lbl_edit_Pyro_tr = list(range(Nb_of_Pyrometer)) - self.lbl_Pyro_model = list(range(Nb_of_Pyrometer)) - self.Pyro_model = list(range(Nb_of_Pyrometer)) - self.lbl_Pyro_Distance = list(range(Nb_of_Pyrometer)) - self.Pyro_Distance = list(range(Nb_of_Pyrometer)) - self.ch_Pilot = list(range(Nb_of_Pyrometer)) - self.lbl_Pyro_t90 = list(range(Nb_of_Pyrometer)) - self.edit_Pyro_t90 = list(range(Nb_of_Pyrometer)) - self.edit_Pyro_array_em = list(range(Nb_of_pyro_array_heads)) - self.lbl_edit_Pyro_array_em = list(range(Nb_of_pyro_array_heads)) - self.lbl_Pyro_array_model = list(range(Nb_of_pyro_array_heads)) - self.Pyro_array_model = list (range(Nb_of_pyro_array_heads)) - self.lbl_Pyro_array_t90 = list(range(Nb_of_pyro_array_heads)) - self.edit_Pyro_array_t90 = list(range(Nb_of_pyro_array_heads)) - self.ch_LSYNC = list(range(Nb_of_DAQ_sensor_types)) - self.ch_OCOM = list(range(Nb_of_DAQ_sensor_types)) - self.ch_AZER = list(range(Nb_of_DAQ_sensor_types)) - self.filter_state = list(range(Nb_of_DAQ_sensor_types)) - self.filter_count = list(range(Nb_of_DAQ_sensor_types)) - self.lbl_filter_state = list(range(Nb_of_DAQ_sensor_types)) - self.lbl_filter_count = list(range(Nb_of_DAQ_sensor_types)) - self.lbl_nplc = list(range(Nb_of_DAQ_sensors)) - self.nplc = list(range(Nb_of_DAQ_sensors)) - - for i in range(Nb_of_DAQ_sensor_types): - self.ch_LSYNC[i] = QCheckBox('LSYNC ' + daq_sensor_types[i]) - if DAQ_lsync: - self.ch_LSYNC[i].setChecked(True) - else: - self.ch_LSYNC[i].setChecked(False) - self.ch_LSYNC[i].setFont(QFont('Times', 12)) - - self.ch_OCOM[i] = QCheckBox('OCOM ' + daq_sensor_types[i]) - if DAQ_ocom: - self.ch_OCOM[i].setChecked(True) - else: - self.ch_OCOM[i].setChecked(False) - self.ch_OCOM[i].setFont(QFont('Times', 12)) - - self.ch_AZER[i] = QCheckBox('AZER ' + daq_sensor_types[i]) - if DAQ_azer: - self.ch_AZER[i].setChecked(True) - else: - self.ch_AZER[i].setChecked(False) - self.ch_AZER[i].setFont(QFont('Times', 12)) - - self.lbl_filter_state[i] = QLabel('Filter ' + daq_sensor_types[i], alignment=Qt.AlignRight) - self.lbl_filter_state[i].setFont(QFont('Times', 12)) - self.filter_state[i] = QComboBox() - self.filter_state[i].setFont(QFont('Times', 12)) - self.filter_state[i].addItem('OFF') - self.filter_state[i].addItem('repeat') - #self.filter_state[i].addItem('moving') - self.lbl_filter_count[i] = QLabel('Counts ' + daq_sensor_types[i], alignment=Qt.AlignRight) - self.lbl_filter_count[i].setFont(QFont('Times', 12)) - self.filter_count[i] = LineEdit() - self.filter_count[i].setFixedWidth(80) - self.filter_count[i].setFont(QFont('Times', 12)) - self.filter_count[i].setEnabled(False) - - for i in range(Nb_of_DAQ_sensors): - self.lbl_nplc[i] = QLabel('NPLC :', alignment=Qt.AlignRight) - self.lbl_nplc[i].setFont(QFont('Times', 12)) - self.nplc[i] = LineEdit() - self.nplc[i].setFixedWidth(80) - self.nplc[i].setFont(QFont('Times', 12)) - self.nplc[i].setText(str(daq_nplc_list[i])) - - for i in range(Nb_of_pyro_array_heads): - self.edit_Pyro_array_em[i] = QComboBox() - self.edit_Pyro_array_em[i].setFont(QFont('Times', 14, QFont.Bold)) - self.edit_Pyro_array_em[i].setFixedWidth(80) - for j in range(10,101): - self.edit_Pyro_array_em[i].addItem(str(j)) - idx = self.edit_Pyro_array_em[i].findText(pyro_array_em_list[i]) - self.edit_Pyro_array_em[i].setCurrentIndex(idx) - self.lbl_Pyro_array_t90[i] = QLabel('t90 [s] : ') - self.lbl_Pyro_array_t90[i].setFont(QFont('Times', 12)) - self.lbl_Pyro_array_t90[i].setAlignment(Qt.AlignRight) - self.edit_Pyro_array_t90[i] = QComboBox() - self.edit_Pyro_array_t90[i].setFont(QFont('Times', 14)) - self.edit_Pyro_array_t90[i].setFixedWidth(80) - s = str(pyro_array_t90_times[i]).replace('[','').replace(']','').replace(',','').replace("'",'').split() - for j in range(len(s)): - self.edit_Pyro_array_t90[i].addItem(str(s[j])) - self.edit_Pyro_array_t90[i].setCurrentIndex(int(pyro_array_t90_list[i])-1) - - self.lbl_edit_Pyro_array_em[i] = QLabel('Emission [%]: ') - self.lbl_edit_Pyro_array_em[i].setFont(QFont('Times', 12)) - self.lbl_edit_Pyro_array_em[i].setAlignment(Qt.AlignRight) - self.lbl_Pyro_array_model[i] = QLabel('Head ' + str(i+1) + ' :') - self.lbl_Pyro_array_model[i].setFont(QFont('Times', 12)) - self.lbl_Pyro_array_model[i].setAlignment(Qt.AlignRight) - self.Pyro_array_model[i] = QLabel(pyro_array_model_list[i]) - self.Pyro_array_model[i].setFont(QFont('Times', 12)) - self.Pyro_array_model[i].setAlignment(Qt.AlignRight) - - for i in range(Nb_of_Pyrometer): - self.ch_Pilot[i] = QCheckBox('Pilot') - self.ch_Pilot[i].setChecked(False) - self.ch_Pilot[i].setFont(QFont('Times', 12)) - - self.edit_Pyro_em[i] = QComboBox() - self.edit_Pyro_em[i].setFont(QFont('Times', 14, QFont.Bold)) - self.edit_Pyro_em[i].setFixedWidth(80) - for j in range(10,101): - self.edit_Pyro_em[i].addItem(str(j)) - idx = self.edit_Pyro_em[i].findText(pyro_em_list[i]) - self.edit_Pyro_em[i].setCurrentIndex(idx) - - self.edit_Pyro_tr[i] = QComboBox() - self.edit_Pyro_tr[i].setFont(QFont('Times', 14, QFont.Bold)) - self.edit_Pyro_tr[i].setFixedWidth(80) - for j in range(10,101): - self.edit_Pyro_tr[i].addItem(str(j)) - idx = self.edit_Pyro_tr[i].findText(pyro_tr_list[i]) - self.edit_Pyro_tr[i].setCurrentIndex(idx) - - self.lbl_Pyro_t90[i] = QLabel('t90 [s] : ') - self.lbl_Pyro_t90[i].setFont(QFont('Times', 12)) - self.lbl_Pyro_t90[i].setAlignment(Qt.AlignRight) - - self.edit_Pyro_t90[i] = QComboBox() - self.edit_Pyro_t90[i].setFont(QFont('Times', 14)) - self.edit_Pyro_t90[i].setFixedWidth(80) - - s = str(pyro_t90_times[i]).replace('[','').replace(']','').replace(',','').replace("'",'').split() - #print (len(s), int(pyro_t90_list[i])-1) - for j in range(len(s)): - self.edit_Pyro_t90[i].addItem(str(s[j])) - self.edit_Pyro_t90[i].setCurrentIndex(int(pyro_t90_list[i])-1) - - self.lbl_edit_Pyro_em[i] = QLabel('Emission [%]: ') - self.lbl_edit_Pyro_em[i].setFont(QFont('Times', 12)) - self.lbl_edit_Pyro_em[i].setAlignment(Qt.AlignRight) - self.lbl_edit_Pyro_tr[i] = QLabel('Transmission [%] :', alignment=Qt.AlignRight) - self.lbl_edit_Pyro_tr[i].setFont(QFont('Times', 12)) - #self.lbl_edit_Pyro_tr[i].setAlignment(Qt.AlignRight) - self.lbl_Pyro_model[i] = QLabel('Model :') - self.lbl_Pyro_model[i].setFont(QFont('Times', 12)) - self.lbl_Pyro_model[i].setAlignment(Qt.AlignRight) - self.Pyro_model[i] = QLabel(pyro_model_list[i]) - self.Pyro_model[i].setFont(QFont('Times', 12)) - self.Pyro_model[i].setAlignment(Qt.AlignRight) - self.lbl_Pyro_Distance[i] = QLabel('Distance :') - self.lbl_Pyro_Distance[i].setFont(QFont('Times', 12)) - self.Pyro_Distance[i] = QLabel(pyro_distance_list[i]) - self.Pyro_Distance[i].setFont(QFont('Times', 12)) - - for i in range(Nb_of_PlotWindows): - self.lbl_x_edit[i] = QLabel('Time [s] : ') - self.lbl_x_edit[i].setFont(QFont('Times', 12)) - self.lbl_x_edit[i].setAlignment(Qt.AlignRight) - self.lbl_y_edit[i] = QLabel('Temp [°C] : ') - idx = gr_idx.index(i) - if sensor_list[idx] == 'Rogowski': - self.lbl_y_edit[i] = QLabel('Current [A] : ') - if sensor_list[idx] == 'DCV-100mV': - self.lbl_y_edit[i] = QLabel('Voltage [mV] : ') - if sensor_list[idx] == 'DCV': - self.lbl_y_edit[i] = QLabel('Voltage [V] : ') - self.lbl_y_edit[i].setFont(QFont('Times', 12)) - self.lbl_y_edit[i].setAlignment(Qt.AlignRight) - - self.edit_x_min[i] = LineEdit() - self.edit_x_min[i].setFixedWidth(90) - self.edit_x_min[i].setFont(QFont('Times', 14, QFont.Bold)) - self.x_min[i]=x_min - self.edit_x_min[i].setText(str("%4.0f" % self.x_min[i])) - self.edit_x_min[i].setEnabled(False) - - self.edit_x_max[i] = LineEdit() - self.edit_x_max[i].setFixedWidth(90) - self.edit_x_max[i].setFont(QFont('Times', 14, QFont.Bold)) - self.x_max[i]=x_max - self.edit_x_max[i].setText(str("%4.0f" % self.x_max[i])) - self.edit_x_max[i].setEnabled(False) - - self.edit_y_min[i] = LineEdit() - self.edit_y_min[i].setFixedWidth(90) - self.edit_y_min[i].setFont(QFont('Times', 14, QFont.Bold)) - self.y_min[i]=y_min - self.edit_y_min[i].setText(str("%4.3f" % self.y_min[i])) - self.edit_y_min[i].setEnabled(False) - - self.edit_y_max[i] = LineEdit() - self.edit_y_max[i].setFixedWidth(90) - self.edit_y_max[i].setFont(QFont('Times', 14, QFont.Bold)) - self.y_max[i]=y_max - self.edit_y_max[i].setText(str("%4.3f" % self.y_max[i])) - self.edit_y_max[i].setEnabled(False) - - self.ch_AutoScale_x[i] = QCheckBox('Autoscale X') - self.ch_AutoScale_x[i].setChecked(True) - self.gr[i].enableAutoRange(axis='x') - self.ch_AutoScale_x[i].setFont(QFont('Times', 12)) - self.ch_AutoScale_x[i].setEnabled(True) - - self.ch_AutoScale_y[i] = QCheckBox('Autoscale Y') - self.ch_AutoScale_y[i].setChecked(True) - self.gr[i].enableAutoRange(axis='y') - self.ch_AutoScale_y[i].setFont(QFont('Times', 12)) - self.ch_AutoScale_y[i].setEnabled(True) - - z_p = 0 - row_idx = [0 for i in range(Nb_of_PlotWindows)] - col_idx = [0 for i in range(Nb_of_PlotWindows)] - - for i in range(Nb_of_PlotWindows): - - row_idx[i] = 6 # for the channels later on - idx = gr_idx.index(i) - if sensor_list[idx] == 'Arduino': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i], 0, 3, 1, 3) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i], 1, 3, 1, 3) - if sensor_list[idx] == 'Pyro': - ParameterGroupLayout[i].addWidget(self.lbl_Pyro_model[z_p], 0, 0) - ParameterGroupLayout[i].setAlignment(self.lbl_Pyro_model[z_p], Qt.AlignBottom|Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.Pyro_model[z_p], 0, 1) - ParameterGroupLayout[i].setAlignment(self.Pyro_model[z_p], Qt.AlignBottom|Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.lbl_Pyro_Distance[z_p], 0, 2) - ParameterGroupLayout[i].setAlignment(self.lbl_Pyro_Distance[z_p], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.Pyro_Distance[z_p], 0, 3) - ParameterGroupLayout[i].setAlignment(self.Pyro_Distance[z_p], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 2, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_y_edit[i], Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 2, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 2, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i], 1, 3, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i], 2, 3, 1, 1) - ParameterGroupLayout[i].addWidget(self.lbl_edit_Pyro_em[z_p], 3, 0) - ParameterGroupLayout[i].setAlignment(self.lbl_edit_Pyro_em[z_p], Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.edit_Pyro_em[z_p], 3, 1) - ParameterGroupLayout[i].addWidget(self.lbl_edit_Pyro_tr[z_p], 3, 2) - ParameterGroupLayout[i].addWidget(self.edit_Pyro_tr[z_p], 3, 3) - ParameterGroupLayout[i].addWidget(self.lbl_Pyro_t90[z_p], 4, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_Pyro_t90[z_p], Qt.AlignLeft) - ParameterGroupLayout[i].addWidget(self.edit_Pyro_t90[z_p], 4, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_Pilot[z_p], 4, 2, 1, 1) - ParameterGroupLayout[i].addWidget(QHLine(), 5, 0, 1, 4) - row_idx[i] = 6 - z_p += 1 - if sensor_list[idx] == 'Pyro_head': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].setAlignment(self.ch_AutoScale_x[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - for z in range(Nb_of_pyro_array_heads): - ParameterGroupLayout[i].addWidget(self.lbl_Pyro_array_model[z], 2+z*2, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_Pyro_array_model[z], Qt.AlignLeft|Qt.AlignBottom) - self.lbl_Pyro_array_model[z].setStyleSheet('color: %s'%self.mycolors[color_list[idx+z]]) - ParameterGroupLayout[i].addWidget(self.Pyro_array_model[z], 2+z*2, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.Pyro_array_model[z], Qt.AlignLeft|Qt.AlignBottom) - self.Pyro_array_model[z].setStyleSheet('color: %s'%self.mycolors[color_list[idx+z]]) - ParameterGroupLayout[i].addWidget(self.lbl_edit_Pyro_array_em[z], 3+z*2, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_Pyro_array_em[z], 3+z*2, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.lbl_Pyro_array_t90[z], 3+z*2, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_Pyro_array_t90[z], 3+z*2, 3, 1, 1) - row_idx[i] = 2 + 2 * Nb_of_pyro_array_heads - ParameterGroupLayout[i].addWidget(QHLine(),row_idx[i], 0, 1, 4) - row_idx[i] = 2 + 2 * Nb_of_pyro_array_heads + 1 - - if sensor_list[idx] == 'TE': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - i_TE = daq_sensor_types.index('TE') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_TE],2,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_OCOM[i_TE],2,1,1,1) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_TE],2,2,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_TE],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_TE],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_TE],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_TE],3,3,1,1) - if sensor_list[idx] == 'PT': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - if '100' in range_list: - i_PT = daq_sensor_types.index('PT-100') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_PT],2,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_OCOM[i_PT],2,1,1,2) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_PT],2,3,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_PT],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_PT],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_PT],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_PT],3,3,1,1) - if '1000' in range_list: - i_PT = daq_sensor_types.index('PT-1000') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_PT],4,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_OCOM[i_PT],4,1,1,2) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_PT],4,3,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_PT],5,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_PT],5,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_PT],5,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_PT],5,3,1,1) - row_idx[i] = 6 - if sensor_list[idx] == 'Rogowski': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].setAlignment(self.ch_AutoScale_x[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - i_Rogowski = daq_sensor_types.index('Rogowski') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_Rogowski],2,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_Rogowski],2,1,1,2) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_Rogowski],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_Rogowski],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_Rogowski],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_Rogowski],3,3,1,1) - ParameterGroupLayout[i].addWidget(QHLine(), 4, 0, 1, 4) - row_idx[i] = 5 - if sensor_list[idx] == 'DCV': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].setAlignment(self.ch_AutoScale_x[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - i_DCV = daq_sensor_types.index('DCV') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_DCV],2,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_DCV],2,1,1,2) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_DCV],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_DCV],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_DCV],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_DCV],3,3,1,1) - if sensor_list[idx] == 'DCV-100mV': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].setAlignment(self.ch_AutoScale_x[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - i_DCV_100mV = daq_sensor_types.index('DCV-100mV') - ParameterGroupLayout[i].addWidget(self.ch_LSYNC[i_DCV_100mV],2,0,1,1) - ParameterGroupLayout[i].addWidget(self.ch_AZER[i_DCV_100mV],2,1,1,2) - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_DCV_100mV],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_DCV_100mV],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_DCV_100mV],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_DCV_100mV],3,3,1,1) - ParameterGroupLayout[i].addWidget(QHLine(), 4, 0, 1, 4) - row_idx[i] = 5 - if sensor_list[idx] == 'ACV': - ParameterGroupLayout[i].addWidget(self.lbl_x_edit[i], 0, 0, 1, 1) - ParameterGroupLayout[i].setAlignment(self.lbl_x_edit[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_min[i], 0, 1, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_min[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.edit_x_max[i], 0, 2, 1, 1) - ParameterGroupLayout[i].setAlignment(self.edit_x_max[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.lbl_y_edit[i], 1, 0, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_min[i], 1, 1, 1, 1) - ParameterGroupLayout[i].addWidget(self.edit_y_max[i], 1, 2, 1, 1) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_x[i],0,3,1,3) - ParameterGroupLayout[i].setAlignment(self.ch_AutoScale_x[i], Qt.AlignBottom) - ParameterGroupLayout[i].addWidget(self.ch_AutoScale_y[i],1,3,1,3) - i_ACV = daq_sensor_types.index('ACV') - ParameterGroupLayout[i].addWidget(self.lbl_filter_state[i_ACV],3,0,1,1) - ParameterGroupLayout[i].addWidget(self.filter_state[i_ACV],3,1,1,1) - ParameterGroupLayout[i].addWidget(self.lbl_filter_count[i_ACV],3,2,1,1, Qt.AlignRight) - ParameterGroupLayout[i].addWidget(self.filter_count[i_ACV],3,3,1,1) - - for i in range(Nb_all_sensors): - #self.lbl_sensor_name[i] = QLabel('C.' + str(ch_list[i])) - self.lbl_sensor_name[i] = QLabel(alias_list[i]) - self.lbl_sensor_name[i].setFont(QFont('Times', 12, QFont.Bold)) - self.lbl_sensor_name[i].setStyleSheet('color: %s'%self.mycolors[color_list[i]]) - self.sensor_value[i] = QLabel('xxx.xxx') - self.sensor_value[i].setFont(QFont('Times', 14, QFont.Bold)) - self.sensor_value[i].setStyleSheet('color: %s' %self.mycolors[color_list[i]]) - self.ch_sensor_value[i] = QCheckBox('Hide') - self.ch_sensor_value[i].setChecked(False) - self.ch_sensor_value[i].setFont(QFont('Times', 12)) - - ParameterGroupLayout[gr_idx[i]].addWidget(self.lbl_sensor_name[i], row_idx[gr_idx[i]], 0) - ParameterGroupLayout[gr_idx[i]].addWidget(self.sensor_value[i], row_idx[gr_idx[i]], 1) - ParameterGroupLayout[gr_idx[i]].addWidget(self.ch_sensor_value[i], row_idx[gr_idx[i]], 2) - - if i < Nb_of_DAQ_sensors: - ParameterGroupLayout[gr_idx[i]].addWidget(self.lbl_nplc[i], row_idx[gr_idx[i]], 3, Qt.AlignRight) - ParameterGroupLayout[gr_idx[i]].addWidget(self.nplc[i], row_idx[gr_idx[i]], 4) - - row_idx[gr_idx[i]] += 1 - - - for i in range(Nb_of_PlotWindows): - self.ch_AutoScale_x[i].clicked.connect(partial(self.update_AutoScale_x, i)) - self.ch_AutoScale_y[i].clicked.connect(partial(self.update_AutoScale_y, i)) - self.edit_x_min[i].editingFinished.connect(partial(self.edit_x_min_changed, i)) - self.edit_x_max[i].editingFinished.connect(partial(self.edit_x_max_changed, i)) - self.edit_y_min[i].editingFinished.connect(partial(self.edit_y_min_changed, i)) - self.edit_y_max[i].editingFinished.connect(partial(self.edit_y_max_changed, i)) - - for i in range(Nb_of_DAQ_sensor_types): - self.ch_LSYNC[i].clicked.connect(partial(self.update_LSYNC, i)) - self.ch_OCOM[i].clicked.connect(partial(self.update_OCOM, i)) - self.ch_AZER[i].clicked.connect(partial(self.update_AZER, i)) - self.filter_state[i].currentIndexChanged.connect(partial(self.update_filter_state, i)) - self.filter_count[i].returnPressed.connect(partial(self.update_filter_count, i)) - - for i in range(Nb_of_DAQ_sensors): - self.nplc[i].returnPressed.connect(partial(self.update_nplc, i)) - #self.nplc[i].editingFinished.connect(partial(self.update_nplc, i)) - - for i in range(Nb_all_sensors): - self.ch_sensor_value[i].clicked.connect(partial(self.update_HideSensor, i)) - - for i in range (Nb_of_Pyrometer): - self.ch_Pilot[i].clicked.connect(partial(self.update_pilot, i)) - self.edit_Pyro_em[i].currentIndexChanged.connect(partial(self.edit_Pyro_em_changed, i)) - self.edit_Pyro_tr[i].currentIndexChanged.connect(partial(self.edit_Pyro_tr_changed, i)) - self.edit_Pyro_t90[i].currentIndexChanged.connect(partial(self.edit_Pyro_t90_changed, i)) - - for i in range (Nb_of_pyro_array_heads): - self.edit_Pyro_array_em[i].currentIndexChanged.connect(partial(self.edit_Pyro_array_em_changed, i)) - self.edit_Pyro_array_t90[i].currentIndexChanged.connect(partial(self.edit_Pyro_array_t90_changed, i)) - - def edit_Pyro_array_t90_changed(self, u): - pyro_array_t90_time[u] = self.edit_Pyro_array_t90[u].currentText() - print (pyro_array_t90_time, pyro_array_t90_time[u]) - idx = self.edit_Pyro_array_t90[u].currentIndex() - Pyrometer_Array.Write_Pyro_Array_Para(u, 't90', str(idx)) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + pyro_array_alias_list[u] + ', t90[s]=' + str(pyro_array_t90_time[u]) + '\n') - ProtocolFile.close() - - def edit_Pyro_t90_changed(self, u): - pyro_t90_time[u] = self.edit_Pyro_t90[u].currentText() - print (pyro_t90_time, pyro_t90_time[u]) - idx = self.edit_Pyro_t90[u].currentIndex() - Pyrometer.Write_Pyro_Para(u, 't90', str(idx)) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + pyro_alias_list[u] + ', t90[s]=' + str(pyro_t90_time[u]) + '\n') - ProtocolFile.close() - - def edit_Pyro_array_em_changed(self, u): - pyro_array_em_list[u] = self.edit_Pyro_array_em[u].currentText() - Pyrometer_Array.Write_Pyro_Array_Para(u, 'e', pyro_array_em_list[u]) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + pyro_array_alias_list[u] + ', emission=' + str(pyro_array_em_list[u]) + '%\n') - ProtocolFile.close() - - def edit_Pyro_em_changed(self, u): - pyro_em_list[u] = self.edit_Pyro_em[u].currentText() - Pyrometer.Write_Pyro_Para(u, 'e', pyro_em_list[u]) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + pyro_alias_list[u] + ', emission=' + str(pyro_em_list[u]) + '%\n') - ProtocolFile.close() - - def edit_Pyro_tr_changed(self, u): - pyro_tr_list[u] = self.edit_Pyro_tr[u].currentText() - Pyrometer.Write_Pyro_Para(u, 't', pyro_tr_list[u]) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + pyro_alias_list[u] + ', transmission=' + str(pyro_tr_list[u]) + '%\n') - ProtocolFile.close() - - def update_pilot(self, u): - Pyrometer.Write_Pilot(u, self.ch_Pilot[u].isChecked()) - - def update_filter_count(self, u): - val_str = self.filter_count[u].text() - val = int(val_str) - self.filter_count[u].clearFocus() - DAQ_6510.Write_Filter_Count(u, val) - - def update_filter_state(self, u): - val_str = self.filter_state[u].currentText() - if val_str == 'OFF': - self.filter_count[u].setEnabled(False) - else: - self.filter_count[u].setEnabled(True) - self.filter_count[u].setText('5') - DAQ_6510.Write_Filter_State(u, val_str) - - def update_nplc(self, u): - val_str = self.nplc[u].text().replace(',', '.') - val = float(val_str) - self.nplc[u].clearFocus() - #self.nplc[u].deselect() - self.nplc[u].setStyleSheet('color: black') - DAQ_6510.Write_NPLC(u, val) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': Sensor ' + daq_alias_list[u] + ', nplc=' + str(val) + '\n') - ProtocolFile.close() - - def update_LSYNC(self, u): - #if daq_sensor_types[u] == 'TE': - #DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - #if daq_sensor_types[u] == 'PT-100': - #DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - #if daq_sensor_types[u] == 'PT-1000': - #DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - #if daq_sensor_types[u] == 'DCV-100mV' or daq_sensor_types[u] == 'DCV': - #DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - #if daq_sensor_types[u] == 'Rogowski': - #DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - DAQ_6510.Write_LSYNC(u, self.ch_LSYNC[u].isChecked()) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - val = self.ch_LSYNC[u].isChecked() - #ProtocolFile.write(time_abs + ': Sensor ' + daq_alias_list[u] + ', LSYNC=' + str(val) + '\n') - ProtocolFile.write(time_abs + ': Sensor type ' + daq_sensor_types[u] + ', LSYNC=' + str(val) + '\n') - ProtocolFile.close() - - def update_OCOM(self, u): - #if daq_sensor_types[u] == 'TE': - #DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - #if daq_sensor_types[u] == 'PT-100': - #DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - #if daq_sensor_types[u] == 'PT-1000': - #DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - #if daq_sensor_types[u] == 'DCV-100mV' or daq_sensor_types[u] == 'DCV': - #DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - #if daq_sensor_types[u] == 'Rogowski': - #DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - DAQ_6510.Write_OCOM(u, self.ch_OCOM[u].isChecked()) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - val = self.ch_OCOM[u].isChecked() - ProtocolFile.write(time_abs + ': Sensor type ' + daq_sensor_types[u] + ', OCOM=' + str(val) + '\n') - ProtocolFile.close() - - - def update_AZER(self, u): - #if daq_sensor_types[u] == 'TE': - #DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - #if daq_sensor_types[u] == 'PT-100': - #DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - #if daq_sensor_types[u] == 'PT-1000': - #DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - #if daq_sensor_types[u] == 'DCV-100mV' or daq_sensor_types[u] == 'DCV': - #DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - #if daq_sensor_types[u] == 'Rogowski': - #DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - DAQ_6510.Write_AZER(u, self.ch_AZER[u].isChecked()) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - val = self.ch_AZER[u].isChecked() - ProtocolFile.write(time_abs + ': Sensor type ' + daq_sensor_types[u] + ', AZER=' + str(val) + '\n') - ProtocolFile.close() - - def calc_x_2_ticks(self, u): - # calculate the datetime axis at the top x axis - delta_t = int(self.x_max[u] - self.x_min[u]) - x2_min = time_start + datetime.timedelta(seconds=self.x_min[u]) - x2_max = (x2_min + datetime.timedelta(seconds=delta_t)).strftime('%H:%M:%S') - x2_list = [] - x2_ticks = [] - for i in range(x2_Nb_ticks): - x2_list.append((x2_min + datetime.timedelta(seconds=i*delta_t/(x2_Nb_ticks-1))).strftime('%H:%M:%S')) - x2_ticks.append([self.x_min[u]+i*delta_t/(x2_Nb_ticks-1), x2_list[i]]) - self.ax_X_2[u].setTicks([x2_ticks,[]]) - - def edit_x_min_changed(self, u): - self.x_min[u] = float(self.edit_x_min[u].text().replace(',','.')) - self.edit_x_min[u].setText(str("%4.0f" % self.x_min[u])) - self.edit_x_min[u].setStyleSheet('color: black') - self.edit_x_min[u].clearFocus() - self.gr[u].setXRange(self.x_min[u], self.x_max[u], padding=self.pdg) - self.calc_x_2_ticks(u) - def edit_x_max_changed(self, u): - self.x_max[u] = float(self.edit_x_max[u].text().replace(',','.')) - self.edit_x_max[u].setText(str("%4.0f" % self.x_max[u])) - self.edit_x_max[u].setStyleSheet('color: black') - self.edit_x_max[u].clearFocus() - self.gr[u].setXRange(self.x_min[u], self.x_max[u], padding=self.pdg) - self.calc_x_2_ticks(u) - def edit_y_min_changed(self, u): - self.y_min[u] = float(self.edit_y_min[u].text().replace(',','.')) - #self.edit_y_min[u].setText(str("%4.5f" % self.y_min[u])) - idx = gr_idx.index(u) - if sensor_list[idx] == 'DCV-100mV': - self.edit_y_min[u].setText(str("%4.5f" % self.y_min[u])) - elif sensor_list[idx] == 'DCV': - self.edit_y_min[u].setText(str("%3.3f" % self.y_min[u])) - else: - self.edit_y_min[u].setText(str("%3.1f" % self.y_min[u])) - self.edit_y_min[u].setStyleSheet('color: black') - self.edit_y_min[u].clearFocus() - self.gr[u].setYRange(self.y_min[u], self.y_max[u], padding=self.pdg) - def edit_y_max_changed(self, u): - self.y_max[u] = float(self.edit_y_max[u].text().replace(',','.')) - #self.edit_y_max[u].setText(str("%4.5f" % self.y_max[u])) - idx = gr_idx.index(u) - if sensor_list[idx] == 'DCV-100mV': - self.edit_y_max[u].setText(str("%4.5f" % self.y_max[u])) - elif sensor_list[idx] == 'DCV': - self.edit_y_max[u].setText(str("%3.3f" % self.y_max[u])) - else: - self.edit_y_max[u].setText(str("%3.1f" % self.y_max[u])) - self.edit_y_max[u].setStyleSheet('color: black') - self.edit_y_max[u].clearFocus() - self.gr[u].setYRange(self.y_min[u], self.y_max[u], padding=self.pdg) - - - def edit_dt_changed(self): - val = int(self.edit_dt.text()) - self.edit_dt.clearFocus() - Sampling_Timer = val - print ('Sampling intervall set to: ', Sampling_Timer, ' ms') - self.timer.setInterval(Sampling_Timer) - ProtocolFile = open(ProtocolFileName, 'a') - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - ProtocolFile.write(time_abs + ': sampling with ' + str(Sampling_Timer) + ' [ms]\n') - ProtocolFile.close() - - - def update_HideSensor(self, u): - if self.ch_sensor_value[u].isChecked(): - print ('Hide sensor ', ch_list[u]) - self.myPen[u] = pg.mkPen(None) - self.pData_line[u].setPen(self.myPen[u]) - self.lbl_sensor_name[u].setEnabled(False) - else: - print ('Sensor ', ch_list[u], ' visible') - self.myPen[u] = pg.mkPen(color=matplotlib.colors.cnames[self.mycolors[color_list[u]]]) - self.pData_line[u].setPen(self.myPen[u]) - self.lbl_sensor_name[u].setEnabled(True) - - def update_AutoScale_x(self, u): - if self.ch_AutoScale_x[u].isChecked(): - self.edit_x_min[u].setEnabled(False) - self.edit_x_max[u].setEnabled(False) - self.gr[u].enableAutoRange(axis='x') - else: - self.edit_x_min[u].setEnabled(True) - self.edit_x_max[u].setEnabled(True) - self.gr[u].disableAutoRange(axis='x') - - def update_AutoScale_y(self, u): - if self.ch_AutoScale_y[u].isChecked(): - self.edit_y_min[u].setEnabled(False) - self.edit_y_max[u].setEnabled(False) - self.gr[u].enableAutoRange(axis='y') - else: - self.edit_y_min[u].setEnabled(True) - self.edit_y_max[u].setEnabled(True) - self.gr[u].disableAutoRange(axis='y') - - - def btn_Start_click(self): - Init_Output_File(ch_list) - self.file_name.setText(FileOutName) - self.lbl_2_start_sampling_time.setText(datetime.datetime.now().strftime('%H:%M:%S')) - self.btn_Exit.setEnabled(True) - self.btn_Pause.setEnabled(True) - self.btn_Start.setEnabled(False) - self.Start_Sampling() - if not args.test: - if DAQ_present: - DAQ_6510.message_daq_display() - - def btn_Exit_click(self): - print ('End of script.') - if not args.test: - if DAQ_present: - DAQ_6510.reset_daq() - sys.exit(0) - - def btn_Pause_click(self): - self.btn_Start.setEnabled(True) - self.btn_Exit.setEnabled(True) - self.btn_Pause.setEnabled(False) - print ('Pause....') - self.timer.stop() - - self.pData_time = np.zeros((0, 2)) # Dim-0 = time/abs, Dim-1=time/s - self.pData_temp = np.empty((0, Nb_all_sensors)) # following Dim's are temperatures - - - x = self.pData_time[:,0].astype(np.float) - for i in range(Nb_all_sensors): - y = self.pData_temp[:,i].astype(np.float) - self.pData_line[i].setData(x, y, connect='finite') - - - def update_graphics(self): - # the main loop to update the graphics output - self.lbl_2_current_time.setText(datetime.datetime.now().strftime('%H:%M:%S')) - time_abs = datetime.datetime.now().strftime('%H:%M:%S') - time_actual = datetime.datetime.now() - global sampling_started, dt_0 - if sampling_started: - # because first sampled time should be 0 - sampling_started = False - dt_0 = (time_actual - time_start).total_seconds() - #print ('dt_0= ',dt_0) - # each sampled step is a new row - # time and temperatures are column items in the new row - new_time = list(range(0)) - dt = (time_actual - time_start).total_seconds() - dt_0 - new_time.append((dt)) - new_time.append((time_abs)) - - # get the actual sensor values from the active instrument(s) - measurement_list = get_measurements() - - new_temp = measurement_list[0] - new_temp_2 = list(range(Nb_all_sensors)) - new_temp_2[:] = new_temp[:] #because of 'call by reference' otherwise - new_ohm = measurement_list[1] #only in PT_1000_mode 'R+T' - if len(new_temp_2) == 0: - print ('Keithley reading timing error, ignoring this sampling step.\n') - else: - for i in range(Nb_all_sensors): - new_temp_2[i] = new_temp_2[i] * float(factor_list[i]) + float(offset_list[i]) - - # matrix pData is for plot - # add each completed sampling step to the matrix as new row - - #if np.nan not in new_temp: - self.pData_time = np.vstack(((self.pData_time), (new_time))) - self.pData_temp = np.vstack(((self.pData_temp), (new_temp_2))) - - x = self.pData_time[:,0].astype(float) - - for i in range(Nb_all_sensors): - y = self.pData_temp[:,i].astype(float) - - #PyQtGraph workaround for NaN from instrument - con = np.isfinite(y) - if len(y) >= 2 and y[-2:-1] != np.nan: - y_ok = y[-2:-1] - y[~con] = y_ok - - self.pData_line[i].setData(x, y, connect = np.logical_and(con, np.roll(con, -1))) - - if sensor_list[i] == 'DCV-100mV': - self.sensor_value[i].setText(str(format(new_temp_2[i], '.6f'))) - else: - self.sensor_value[i].setText(str(format(new_temp_2[i], '.3f'))) - - # check scaling in the graph - g_i = gr_idx[i] - self.ax_X[g_i] = self.gr[g_i].getAxis('bottom') - self.ax_X_2[g_i] = self.gr[g_i].getAxis('top') - self.ax_Y[g_i] = self.gr[g_i].getAxis('left') - if self.ch_AutoScale_x[g_i].isChecked(): - self.x_min[g_i] = self.ax_X[g_i].range[0] - self.x_max[g_i] = self.ax_X[g_i].range[1] - self.edit_x_min[g_i].setText(str("%4.0f" % self.x_min[g_i])) - self.edit_x_max[g_i].setText(str("%4.0f" % self.x_max[g_i])) - self.calc_x_2_ticks(g_i) - if self.ch_AutoScale_y[g_i].isChecked(): - self.y_min[g_i] = self.ax_Y[g_i].range[0] - self.y_max[g_i] = self.ax_Y[g_i].range[1] - if sensor_list[i] == 'DCV-100mV': - self.edit_y_min[g_i].setText(str("%3.5f" % self.y_min[g_i])) - self.edit_y_max[g_i].setText(str("%3.5f" % self.y_max[g_i])) - elif sensor_list[i] == "DCV": - self.edit_y_min[g_i].setText(str("%3.3f" % self.y_min[g_i])) - self.edit_y_max[g_i].setText(str("%3.3f" % self.y_max[g_i])) - else: - self.edit_y_min[g_i].setText(str("%4.1f" % self.y_min[g_i])) - self.edit_y_max[g_i].setText(str("%4.1f" % self.y_max[g_i])) - - - - # write (append) each sampling to the output file - OutputFile = open(FileOutName, 'a') - OutputFile.write(str(time_abs)) - OutputFile.write(str(format(dt, '.3f')).rjust(12)) - z = 0 - for i in range(Nb_all_sensors): - if sensor_list[i] == 'DCV-100mV': - OutputFile.write(str(format(new_temp_2[i], '.6f')).rjust(14)) - else: - OutputFile.write(str(format(new_temp_2[i], '.3f')).rjust(14)) - if sensor_list[i] == 'PT' and range_list[i] == '1000' and PT_1000_mode == 'R+T': - OutputFile.write(str(format(new_ohm[z], '.4f')).rjust(12)) - z += 1 - - - OutputFile.write('\n') - OutputFile.close() - - def Start_Sampling(self): - # START button pressed - global time_start, sampling_started - print ('Start sampling.') - time_start = datetime.datetime.now() - x2_min = time_start - self.timer.start() - sampling_started = True - - - gr_app = QApplication(sys.argv) - gr = grPanel() - - screen_width = gr_app.desktop().screenGeometry().width() - screen_height = gr_app.desktop().screenGeometry().height() - if screen_width == 1280: - gr.resize(1180,900) - gr.move(10, 10) - else: - gr.resize(1400,1100) - gr.move(400, 10) - #gr.resize(700,500) - #gr.move(10,10) - gr.show() - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QApplication.instance().exec_() - - return() - - - -# start the GUI -Graphics() - - -print ('Sampling done ... exit script...') - - -