#############-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('', 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