#############-Introduction-#############
# This code is used to control a stirred tank reactor. Its main functions are
# - Creating a GUI
# - Sending instructions to the arduino modules
# - Receiving data from the arduinos
# - Displaying and saving the data
# Published by Julian Zoller at 4th april 2026



#############-Libraries-#############
import customtkinter as ctk # for graphical user interface
import tkinter as tk # for graphical user interface
from tkinter import ttk # for graphical user interface
from PIL import Image, ImageTk # The pillow library is needed for pictures
import serial # for serial communication with arduino
from datetime import datetime # for nomenclature or filename including date and time
import time # for calculation of the time scince the program started
import csv # for handling csv files
import matplotlib # for mathematical figures/ plots
matplotlib.use("TkAgg") #backend of matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk #FigureCanvasTkAgg is used to plot the figure on a tkinter canvas, NavigationToolbar2Tk is the matplotlib standard navigation toolbar which is nice to have
from matplotlib.figure import Figure # for mathematical figures/ plots
import matplotlib.animation as animation # used for live data update of mathematical figures / plots



################################################
##########-create data file-####################
################################################
now = datetime.now() # get current timestamp used in the filename
lasttime_Ard_Cond = 0 #used to ensure consecutive data points in the data of arduino 1
lasttime_Ard_Heat = 0 #used to ensure consecutive data points in the data of arduino 2
Ard_Cond_Values = [] #used to store actual received data
Ard_Heat_Values = [] #used to store actual received data
fieldnames = ["t_computer / s","t_Ard_Cond / s", "conductivity / (mS/cm)","T3 / °C", "t_Ard_Heat / s", "T1 / °C", "T2 / °C", "T1_set / °C"] #name of table header
Filename=now.strftime("batch_reactor_%Y%m%d_%H%M%S.csv") #creating filename using strftime to include date and time
print('Data is saved in: ', Filename) # print filename to terminal

#create csv file and writing header into it
with open(Filename, 'w', newline='') as csv_file:
    csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
    csv_writer.writeheader()



#############-Definitions-#############
title_font = ('Arial', 14, 'bold') #font for titles in the GUI
text_font = ('Arial', 14) #font for text in the GUI
Step = 'Fill' #actual step during automatic operation
start_time = time.time() #start time of the program
start_drain_time = time.time() #time at which draining of the reactor is started (used for automatic operation in which the reactor is drained for a certain time period)



################################################
##########-setup serial communication-##########
################################################
Ard_Pump_Stirr = serial.Serial(port='COM9', baudrate=9600) # Configure serial communication and create object for communication with arduino
Ard_Syringe = serial.Serial(port='COM5', baudrate=9600)    # Configure serial communication and create object for communication with arduino
Ard_Cond = serial.Serial(port='COM10', baudrate=9600)      # Configure serial communication and create object for communication with arduino
Ard_Heat = serial.Serial(port='COM6', baudrate=9600)       # Configure serial communication and create object for communication with arduino


#there may be old datasets with high timestamps present in the serial communication lines
#these while loops flush the serial communication lines
while Ard_Pump_Stirr.in_waiting:
    line = Ard_Pump_Stirr.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
while Ard_Syringe.in_waiting:
    line = Ard_Syringe.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
while Ard_Cond.in_waiting:
    line = Ard_Cond.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
while Ard_Heat.in_waiting:
    line = Ard_Cond.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
print("Cleared serial communication buffer")



#############-GUI-Window-#############
app = ctk.CTk() # create a CTk object, which is a window for the app you want to create
app.title('Batch Reactor') # set the text appearing in the upper window bar of the window
app.geometry('1500x775+0+0') # set the default size of the window in pixels



#############-Functions-#############
def resize_PID(event): # function to resize the Process Instrumentation Diagram (PID) image, when the window is resized
	global resized_tk
	PID_canvas_ratio = event.width / event.height # current ratio 
	height = int(event.height)
	width = int(event.width)    
	resized_image = PID_original.resize((width,height))
	resized_tk = ImageTk.PhotoImage(resized_image)
	PID_canvas.create_image(
		int(event.width / 2),
		int(event.height / 2),
		anchor = 'center',
		image = resized_tk)

def SendInstructions(Arduino,Arduinoname,Actor,Actorvalue):                 # Send instructions to an Arduino using the Arduino object for serial communication, the Arduinos name, the actor name and the value the actor should have
    Message = '<' + Arduinoname + ',' + Actor + ',' + str(Actorvalue) + '>' # define message to be sent
    print(Message)
    Arduino.write(Message.encode())                                         # sending message using serial communication

def readserial_Ard_Syringe(): # read messages send by the Ard_Syringe and set the indicator for the filling of the syringe
    # The received Message has the form "max_fill_button,1,min_fill_button,1" with 0 meaning contact has been made to the according button
    line1 = Ard_Syringe.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
    sensorValues1 = line1.split(',') #Split line into a string using comma's as separator
    if sensorValues1[1] == '0': #if the limit switch is pressed
          syringe_label_level.configure(text = 'Full') #reconfigure the label on the GUI
    elif sensorValues1[3] == '0': #if the limit switch is pressed
          syringe_label_level.configure(text = 'Empty') #reconfigure the label on the GUI
          SendInstructions(Ard_Syringe,'Ard_Syringe','Syringe','-6') #when the syringe is empty it is filled up automatically
    else:
          syringe_label_level.configure(text = '') #reconfigure the label on the GUI
    app.after(1000, readserial_Ard_Syringe) # repeat this function every 1000 ms

def readserial_Ard_Cond(): # read messages send by the Ard_Cond
    global Ard_Cond_Values
    # The received message has the form "time,378,Conductivity,0.00,T3,23.67" with time in s, conductivity in mS/cm and T3 in °C
    line1 = Ard_Cond.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
    Ard_Cond_Values = line1.split(',') #Split line into a string using comma's as separator
    conductivity.set(value=Ard_Cond_Values[3]) #set the tkinter variable for conductivity, which is displayed on the GUI
    T3.set(value=Ard_Cond_Values[5]) #set the tkinter variable for T3, which is displayed on the GUI
    app.after(1000, readserial_Ard_Cond) # repeat this function every 1000 ms

def readserial_Ard_Heat(): # read messages send by the Ard_Heat
    global Ard_Heat_Values
    # The received message has the form "time_Ard_Heat,41,T1,23.40,T2,23.14,T1_set,24.60,heater_status,0" with time in s, and the temperatures in °C
    line1 = Ard_Heat.readline().decode('utf-8').strip() #decode signal from utf-8 and remove any leading or trailing whitespace
    Ard_Heat_Values = line1.split(',') #Split line into a string using comma's as separator
    T1.set(value=Ard_Heat_Values[3]) #set the tkinter variable, which is displayed on the GUI
    T2.set(value=Ard_Heat_Values[5]) #set the tkinter variable, which is displayed on the GUI
    if Ard_Heat_Values[9] == '1': #if the heater status is 1
          heater_status_label.configure(text = 'ON', text_color = 'red') #reconfigure the label on the GUI
    else:
          heater_status_label.configure(text = 'OFF', text_color = 'black') #reconfigure the label on the GUI
    app.after(1000, readserial_Ard_Heat) # repeat this function every 1000 ms

def saveData(): # read messages send by the arduinos
    #the actual data is saved in global variables by the readserial functions so that it can be accessed by this function for saving it into a textfile
    global lasttime_Ard_Cond
    global lasttime_Ard_Heat
    global Ard_Cond_Values
    global Ard_Heat_Values 
    
    #exclude incomplete datasets for simpler plotting
    if(((len(Ard_Cond_Values))>=6) and ((len(Ard_Heat_Values))>=10)):
        #exclude too small timestamps for simpler plotting
        if((float(Ard_Cond_Values[1])>=lasttime_Ard_Cond) and (float(Ard_Heat_Values[1])>=lasttime_Ard_Heat)):
            #write data to file
            with open(Filename, 'a', newline='') as csv_file:
                csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
                info = {
                        "t_computer / s": (time.time()-start_time), # timestamp of the computer in seconds
                        "t_Ard_Cond / s": Ard_Cond_Values [1], # timestep of arduino1 t in seconds
                        "conductivity / (mS/cm)": Ard_Cond_Values [3], # sensor value of arduino 1
                        "T3 / °C": Ard_Cond_Values [5], # sensor value of arduino 1
                        "t_Ard_Heat / s": Ard_Heat_Values [1], # timestep of arduino2 t in seconds
                        "T1 / °C": Ard_Heat_Values [3], # sensor value of arduino 2
                        "T2 / °C": Ard_Heat_Values [5], # sensor value of arduino 2
                        "T1_set / °C": Ard_Heat_Values [7], # sensor value of arduino 2
                } 
                lasttime_Ard_Cond=float(Ard_Cond_Values[1])
                lasttime_Ard_Heat=float(Ard_Heat_Values[1])
                csv_writer.writerow(info)
    app.after(1000, saveData) # repeat this function every 1000 ms

def animate(i): # function to update the figure in a defined time interval
    pullData = open(Filename,"r").read() # open the file which's data should be plotted
    dataList = pullData.split('\n') # create a list in which every line in the data file is a new element
    t_List = [] # list to store the x values in
    conductivity_List = [] # list to store the y values in
    T3_List = [] # list to store the y values in
    time_Ard_Heat_List = [] # list to store the x values in
    T1_List = [] # list to store the y values in
    T2_List = [] # list to store the y values in
    T1_set_List = [] # list to store the y values in
    for eachLine in dataList[1:]: # copy the data from the file in xList and yList with a loop over each element in dataList excluding the header
        if len(eachLine) > 1: # exclude empty lines, which may appear in data
            t,t1,cond,Temp3,t2,Temp1,Temp2,Temp1_set = eachLine.split(',') # each line in the data file/ element in the data list contains the values separated by ','
            t_List.append(float(t)) # add the x value of the current line to xList
            conductivity_List.append(float(cond)) # add the y value of the current line to yList
            T3_List.append(float(Temp3)) # add the y value of the current line to yList
            T1_List.append(float(Temp1)) # add the y value of the current line to yList
            T2_List.append(float(Temp2)) # add the y value of the current line to yList
            T1_set_List.append(float(Temp1_set)) # add the y value of the current line to yList
    subplot1.clear() # clear the plot, before plotting something new. If you not clear it draws over and over again the data reducing performance
    subplot1.plot(t_List,conductivity_List, 'b')
    subplot1.set_xlabel("time / s", fontsize=12)
    subplot1.set_ylabel("conductivity / (mS/cm)", fontsize=12)
    subplot1.tick_params(axis='both', labelsize=12)
    subplot2.clear() # clear the plot, before plotting something new. If you not clear it draws over and over again the data reducing performance
    subplot2.plot(t_List,T1_List,'r', label="T1")
    subplot2.plot(t_List,T2_List,'g', label="T2")
    subplot2.plot(t_List,T3_List,'b', label="T3")
    subplot2.plot(t_List,T1_set_List,'m', label="T1_set")
    subplot2.set_xlabel("time / s", fontsize=12)
    subplot2.set_ylabel("temperature / °C", fontsize=12)
    subplot2.tick_params(axis='both', labelsize=12)
    subplot2.legend(loc="upper left", fontsize=12)
 
def control_cond(): #control conductivity in the reactor by injecting a reagent with the syringe, which can change it
    if syringe_control_selection.get() == '1': #if the conductivity control is activated in the GUI
        #determine the set conductivity by the tkinter entry field variable set_conductivity
        if set_conductivity.get() == '':
             set_cond = 0.0
        else:
             set_cond = float(set_conductivity.get())
        #determine the actual conductivity
        actual_cond = float(conductivity.get())
        #determine the conductivity of the medium which is injected using the tkinter entry field variable medium_conductivity
        if medium_conductivity.get() == '':
             medium_cond = 0.0
        else:
             medium_cond = float(medium_conductivity.get())
        if medium_cond - set_cond != 0: #if the conductivity can be changed by injecting medium
             V_mix = 327.0+float(set_fill_power.get())/100.0*60.0 #calculate the volume of fluid, whichs conductivity should be changed
             V_inject = (set_cond-actual_cond)/(medium_cond-set_cond)*V_mix #calculate the volume which must be injected
             if V_inject > 0:
                  SendInstructions(Ard_Syringe,'Ard_Syringe','Syringe',str(V_inject)) #inject the medium
    app.after(30000, control_cond) # repeat this function every 30000 ms
     
def automatic_mode(): #this function is used for automatic operation of the batch reactor
    global Step #this variable indicates in which step the automatic operation is
    global start_drain_time #time at which draining of the reactor is started
    if mode_var.get() == 2: #if the tkinter variable for the radiobuttons mode_var indicates that automatic operation is activated
          if Step == 'Drain':
               #indicate the current step, by changing the colors of the labels
               step4_label.configure(text_color = 'black')
               step5_label.configure(text_color = 'red')
               reactor_stirrer_switch.deselect() #turn off the reactor stirrer
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','reactor',reactor_stirrer_switch.get())
               bath_stirrer_switch.deselect() #turn off the bath stirrer
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','bath',bath_stirrer_switch.get())
               set_drain_power.set('100') #turn on the pump which drains the reactor
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','drain',set_drain_power.get())
               if (time.time() - start_drain_time) > 200.0: #after the reactor has benn drained for 200 s
                    set_drain_power.set('0') #turn off the drain pump
                    SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','drain',set_drain_power.get())
                    mode_var.set(1) #leave automatic operation
          if Step == 'React':
               #indicate the current step, by changing the colors of the labels
               step3_label.configure(text_color = 'black')
               step4_label.configure(text_color = 'red')
               if float(conductivity.get()) < 14.2: #if the reaction is finished, which is indicated by a drop of conductivity
                    Step = 'Drain' #go to the next step
                    start_drain_time = time.time()
          if Step == 'Inject':
               #indicate the current step, by changing the colors of the labels
               step2_label.configure(text_color = 'black')
               step3_label.configure(text_color = 'red')
               reactor_stirrer_switch.select() #activate the reactor stirrer
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','reactor',reactor_stirrer_switch.get())
               SendInstructions(Ard_Syringe,'Ard_Syringe','Syringe',syringe_value.get()) #inject the amount of liquid with the syringe, which is defined in the GUI entry field
               Step = 'React' #go to the next step
          if Step == 'Condition':
               #indicate the current step, by changing the colors of the labels
               step1_label.configure(text_color = 'black')
               step2_label.configure(text_color = 'red')
               set_fill_power.set('0') #stop the fill pump
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','fill',set_fill_power.get())
               SendInstructions(Ard_Heat,'Ard_Heat','Heater',set_temperature.get()) #start the heater with the target temperature specified in the GUI entry field
               bath_stirrer_switch.select() #start the bath stirrer
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','bath',bath_stirrer_switch.get())
               if float(T1.get()) > float(set_temperature.get()): #when the target temperature has been reached
                    Step = 'Inject' #go to the next step
          if Step == 'Fill':
               #indicate the current step, by changing the colors of the labels
               step5_label.configure(text_color = 'black')
               step1_label.configure(text_color = 'red')
               set_fill_power.set('100') #activate the pump which fills the reactor
               SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','fill',set_fill_power.get())
               if float(conductivity.get()) > 15.2: #if the liquid level has reached the conductivity sensor
                    Step = 'Condition' #go to the next step
    if mode_var.get() == 1: #if the tkinter variable for the radiobuttons mode_var indicates that manual operation is activated
          #indicate the current step, by changing the colors of the labels
          step1_label.configure(text_color = 'black')
          step2_label.configure(text_color = 'black')
          step3_label.configure(text_color = 'black')
          step4_label.configure(text_color = 'black')
          step5_label.configure(text_color = 'black')
          Step = 'Fill' #reset the step for automatic operation to "fill"
    app.after(5000,automatic_mode) #repeat this function every 5 s

        

#############-GUI-Widgets-#############
# PID Diagram on a canvas
PID_canvas = tk.Canvas(app, background = 'white', bd = 0, highlightthickness = 0, relief = 'ridge') #get rid of the boundary of the canvas using bd, highlightthickness and relief
PID_canvas.grid(column = 0, row = 1, columnspan = 7, rowspan = 9, sticky = 'nsew', padx = 22, pady = 15) #place the canvas in the according fields of the grid defined in the layout section
PID_original = Image.open('C:/Users/Admin/Documents/Python_Codes2/PID.png') # import the image, if the image is in the same folder as the programm, only the file-path is needed
image_ratio = PID_original.size[0] / PID_original.size[1] #image ratio of the original image used later to resize the image
#the image is plotted on the function by the resize_PID function

# Ard_Syringe widgets
syringe_button = ctk.CTkButton(master = PID_canvas, command = lambda: SendInstructions(Ard_Syringe,'Ard_Syringe','Syringe',syringe_value.get()), text="Inject", font = text_font, width = 40,) #button for injecting medium in the reactor
syringe_button.place(relx = 0.45, rely = 0.08, anchor = 'nw')
syringe_value = tk.StringVar(value = '0')
syringe_entry = ctk.CTkEntry(master = PID_canvas, textvariable = syringe_value, width = 35, font = text_font) #entry field to specify how much liquid should be injected
syringe_entry.place(relx = 0.50, rely = 0.08, anchor = 'nw')
syringe_label = ctk.CTkLabel(master = PID_canvas, text = 'ml', width = 25, font = text_font)
syringe_label.place(relx = 0.54, rely = 0.08, anchor = 'nw')
syringe_label_level = ctk.CTkLabel(master = PID_canvas, text = '', width = 25, font = text_font, text_color = 'red') #label used to indicate if the syringe is full or empty
syringe_label_level.place(relx = 0.45, rely = 0.12, anchor = 'nw')
syringe_control_selection = tk.StringVar(value = '0')
syringe_control_switch = ctk.CTkSwitch(master = PID_canvas, variable = syringe_control_selection, text="Control cond.", onvalue="1", offvalue="0", font = text_font) #switch to turn on the control_cond function
syringe_control_switch.place(relx = 0.45, rely = 0.16, anchor = 'nw')
set_conductivity = tk.StringVar(value = '0')
syringe_entry = ctk.CTkEntry(master = PID_canvas, textvariable = set_conductivity, width = 40, font = text_font) #entry field to indicate the set conductivity for conductivity control
syringe_entry.place(relx = 0.45, rely = 0.20, anchor = 'nw')
syringe_entry_label = ctk.CTkLabel(master = PID_canvas, text = 'mS/cm', width = 20, font = text_font)
syringe_entry_label.place(relx = 0.49, rely = 0.20, anchor = 'nw')
Medium_select_label = ctk.CTkLabel(master = PID_canvas, text = 'Medium:', width = 20, font = text_font)
Medium_select_label.place(relx = 0.185, rely = 0.05, anchor = 'nw')
medium_conductivity = tk.StringVar(value = '0')
medium_entry = ctk.CTkEntry(master = PID_canvas, textvariable = medium_conductivity, width = 40, font = text_font) #entry field to indicate the medium conductivity which is needed by the control_cond function
medium_entry.place(relx = 0.185, rely = 0.08, anchor = 'nw')
medium_entry_label = ctk.CTkLabel(master = PID_canvas, text = 'mS/cm', width = 20, font = text_font)
medium_entry_label.place(relx = 0.23, rely = 0.08, anchor = 'nw')

# Ard_Pump_Stirr switches
fill_button = ctk.CTkButton(master = PID_canvas, command = lambda: SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','fill',set_fill_power.get()),  text="Set power to", width = 30, font = text_font) #button to send the specified pump power to the arduino
fill_button.place(relx = 0.30, rely = 0.36, anchor = 'nw')
set_fill_power = tk.StringVar(value = '0')
fill_entry = ctk.CTkEntry(master = PID_canvas, textvariable = set_fill_power, width = 40, font = text_font) #entry field to specify the pump power
fill_entry.place(relx = 0.30, rely = 0.4, anchor = 'nw')
fill_entry_label = ctk.CTkLabel(master = PID_canvas, text = '%', width = 20, font = text_font)
fill_entry_label.place(relx = 0.34, rely = 0.4, anchor = 'nw')
drain_button = ctk.CTkButton(master = PID_canvas, command = lambda: SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','drain',set_drain_power.get()), text="Set power to", width = 30, font = text_font) #button to send the specified pump power to the arduino
drain_button.place(relx = 0.29, rely = 0.53, anchor = 'nw')
set_drain_power = tk.StringVar(value = '0')
drain_entry = ctk.CTkEntry(master = PID_canvas, textvariable = set_drain_power, width = 40, font = text_font) #entry field to specify the pump power
drain_entry.place(relx = 0.29, rely = 0.57, anchor = 'nw')
drain_entry_label = ctk.CTkLabel(master = PID_canvas, text = '%', width = 20, font = text_font)
drain_entry_label.place(relx = 0.33, rely = 0.57, anchor = 'nw')
reactor_stirrer_switch = ctk.CTkSwitch(master = PID_canvas, command = lambda: SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','reactor',reactor_stirrer_switch.get()), text="ON", onvalue="1", offvalue="0", font = text_font) #switch to activate or deactivate the reactor stirrer
reactor_stirrer_switch.place(relx = 0.70, rely = 0.08, anchor = 'nw')
bath_stirrer_switch = ctk.CTkSwitch(master = PID_canvas, command = lambda: SendInstructions(Ard_Pump_Stirr,'Ard_Pump_Stirr','bath',bath_stirrer_switch.get()), text="ON", onvalue="1", offvalue="0", font = text_font) #switch to activate or deactivate the bath stirrer
bath_stirrer_switch.place(relx = 0.90, rely = 0.28, anchor = 'nw')

# Heater Switch Entry
heater_button = ctk.CTkButton(master = PID_canvas, command = lambda: SendInstructions(Ard_Heat,'Ard_Heat','Heater',set_temperature.get()), text="Set T1 to", width = 30, font = text_font) #button to send the target temperature specified in the entry field to the arduino
heater_button.place(relx = 0.66, rely = 0.85, anchor = 'nw')
temperature_entry_label = ctk.CTkLabel(master = PID_canvas, text = '°C', width = 20, font = text_font)
temperature_entry_label.place(relx = 0.78, rely = 0.85, anchor = 'nw')
set_temperature = tk.StringVar(value = '0')
temperature_entry = ctk.CTkEntry(master = PID_canvas, textvariable = set_temperature, width = 30, font = text_font) #entry field for the target temperature
temperature_entry.place(relx = 0.75, rely = 0.85, anchor = 'nw')
heater_status_label = ctk.CTkLabel(master = PID_canvas, text = '', width = 25, height = 10, font = text_font, text_color = 'red') #label that indicates the heater operation
heater_status_label.place(relx = 0.72, rely = 0.9, anchor = 'nw')

# Measurement labels
T1 = tk.StringVar(value = '0')
T1_label = ctk.CTkLabel(master = PID_canvas, text = '0', textvariable = T1, width = 25, font = text_font) #label to show the measured variable in the GUI
T1_label.place(relx = 0.44, rely = 0.77, anchor = 'ne')
T1_unit = ctk.CTkLabel(master = PID_canvas, text = ' °C', width = 25, font = text_font)
T1_unit.place(relx = 0.43, rely = 0.77, anchor = 'nw')
T2 = tk.StringVar(value = '0')
T2_label = ctk.CTkLabel(master = PID_canvas, text = '0', textvariable = T1, width = 25, font = text_font) #label to show the measured variable in the GUI
T2_label.place(relx = 0.54, rely = 0.44, anchor = 'ne')
T2_unit = ctk.CTkLabel(master = PID_canvas, text = ' °C', width = 25, font = text_font)
T2_unit.place(relx = 0.53, rely = 0.44, anchor = 'nw')
T3 = tk.StringVar(value = '0')
T3_label = ctk.CTkLabel(master = PID_canvas, text = '0', textvariable = T3, width = 25, font = text_font) #label to show the measured variable in the GUI
T3_label.place(relx = 0.92, rely = 0.10, anchor = 'ne')
T3_unit = ctk.CTkLabel(master = PID_canvas, text = '°C', width = 25, font = text_font)
T3_unit.place(relx = 0.92, rely = 0.10, anchor = 'nw')
conductivity = tk.StringVar(value = '0')
A_label = ctk.CTkLabel(master = PID_canvas, text = '0', textvariable = conductivity, width = 25, font = text_font) #label to show the measured variable in the GUI
A_label.place(relx = 0.82, rely = 0.13, anchor = 'ne')
A_label = ctk.CTkLabel(master = PID_canvas, text = 'mS/cm', width = 25, font = text_font)
A_label.place(relx = 0.825, rely = 0.13, anchor = 'nw')

# Logo
Logo_frame = ctk.CTkFrame(master = app, fg_color = 'transparent') #create a frame in which the Logo text labels can be packed
Logo_frame.grid(column = 8, row = 0, columnspan = 2, padx = 10, pady = 0, sticky = 'n')
Logo1 = ctk.CTkLabel(master = Logo_frame, text = 'Zoller', font = ('Segoe Print', 50, 'bold'), text_color = '#00B0F0', width = 25) #label for first part of the logo
Logo1.pack(side = 'left')
Logo2 = ctk.CTkLabel(master = Logo_frame, text = 'LAB', font = ('Arial Black', 50, 'bold'), text_color = '#00B0F0', width = 25) #label for second part of the logo
Logo2.pack(side = 'left')

# Mode selection
mode_frame = ctk.CTkFrame(master = app) #create a frame in the grid to pack the radiobuttons for mode selection in
mode_frame.grid(column = 0, row = 0, padx = 10, pady = 0, sticky = 'n')
mode_frame_label = ctk.CTkLabel(master = mode_frame, text = 'Operation Mode', font = title_font, width = 25)
mode_frame_label.grid(column = 0, row = 0, sticky = 'nesw')
#create two radiobuttons which can change the tkinter variable mode_var to indicate if manual or automatic mode is selected
mode_var = tk.IntVar(value=1)
radiobutton_1 = ctk.CTkRadioButton(master = mode_frame, text="Manual",
								font = text_font,
                                variable= mode_var, # set radio_var to value = 1 when the radiobutton is pressed
                                value=1) # number of the radio button, when mode_var contains this number the radio button is on
radiobutton_1.grid(column = 0, row = 1, sticky = 'w')
radiobutton_2 = ctk.CTkRadioButton(master = mode_frame, text="Automatic",
								font = text_font,
                                variable= mode_var, # set radio_var to value = 2 when the radiobutton is pressed
                                value=2) # number of the radio button, when mode_var contains this number the radio button is on
radiobutton_2.grid(column = 0, row = 2, sticky = 'w')

# Plot
plot_frame = ctk.CTkFrame(master = app) #create a frame in which the plots can be placed
plot_frame.grid(column = 7, row = 1, columnspan = 3, rowspan = 9, sticky = 'nsew', padx = 22, pady = 15)
# Create the matplotlib figure
plot = Figure(figsize=(5,5), dpi=100, constrained_layout=True) #constrained_layout is used so that xlabel and ylabel are not pushed outside of the figure
subplot1 = plot.add_subplot(211)
subplot2 = plot.add_subplot(212)
plot_canvas = FigureCanvasTkAgg(plot, plot_frame)
plot_canvas.get_tk_widget().pack(fill='both', expand=True) # Draw the figure on a Canvas

# Step indication during automatic operation
step_frame = ctk.CTkFrame(master = app) #create a frame which contains the labels for the different steps during automatic operation
step_frame.grid(column = 1, row = 0, columnspan = 5, padx = 10, pady = 0, sticky = 'n')
step_frame_label = ctk.CTkLabel(master = step_frame, text = 'Step during automatic operation', font = title_font)
step_frame_label.grid(column = 0, row = 0, columnspan = 2, sticky = 'w')
step1_label = ctk.CTkLabel(master = step_frame, text = ' 1. Fill      ', fg_color = '#A6A6A6', font = text_font)
step1_label.grid(column = 0, row = 1)
step2_label = ctk.CTkLabel(master = step_frame, text = ' 2. Condition ', fg_color = '#A6A6A6', font = text_font)
step2_label.grid(column = 1, row = 1)
step3_label = ctk.CTkLabel(master = step_frame, text = ' 3. Inject    ', fg_color = '#A6A6A6', font = text_font)
step3_label.grid(column = 2, row = 1)
step4_label = ctk.CTkLabel(master = step_frame, text = ' 4. Reaction  ', fg_color = '#A6A6A6', font = text_font)
step4_label.grid(column = 3, row = 1)
step5_label = ctk.CTkLabel(master = step_frame, text = ' 5. Drain     ', fg_color = '#A6A6A6', font = text_font)
step5_label.grid(column = 4, row = 1)



#############-GUI-Layout-#############
app.columnconfigure((0,1,2,3,4,5,6,7,8,9), weight = 1, uniform = 'a') #configure 2 columns with equal width
app.rowconfigure((0,1,2,3,4,5,6,7,8,9), weight = 1, uniform = 'a') #index of row, relative width is the weight

mode_frame.columnconfigure(0, weight = 1, uniform = 'b')
mode_frame.rowconfigure((0,1,2), weight = 1, uniform = 'b')

step_frame.columnconfigure((0,1,2,3,4), weight = 1, uniform = 'c')
step_frame.rowconfigure((0,1), weight = 1, uniform = 'c')



#############-Loops-#############
PID_canvas.bind('<Configure>', resize_PID) #when the size of the canvas changes this function is called
readserial_Ard_Syringe() #function call (the function calls itself afterwards)
readserial_Ard_Cond() #function call (the function calls itself afterwards)
readserial_Ard_Heat() #function call (the function calls itself afterwards)
saveData() #function call (the function calls itself afterwards)
ani = animation.FuncAnimation(plot, animate, interval = 1000) # read the data file and update the plots. update interval in ms
control_cond() #function call (the function calls itself afterwards)
automatic_mode() #function call (the function calls itself afterwards)
app.mainloop() #main loop of the ctk GUI