Hydroponic Controller

Application & Hardware

A hydroponic controller build used to control & monitor electrical conductivity, pH, temperature, and circulation pumps. The initial build was simpler and ran using the command line interface (CLI). The project was further developed to include a graphical user interface (GUI), data display, and data logging.

Application

Command Line Interface (CLI) Version

    		
import os
import RPi.GPIO as GPIO
import smbus
import string
import sys
import time
sys.path.insert(0,'/home/pi/.local/lib/python3.7/site-packages')
from simple_pid import PID

#initialize I2C bus and addresses using hexidecimal
bus = smbus.SMBus(1)
ph_address = 0x63
ec_address = 0x64
temp_address = 0x66
doser1 = 0xA
doser2 = 0xB
doser3 = 0xC
doser4 = 0xD

#GPIO configuration
def GPIO_configuration():
    global relay_magstir,relay_main_pump,relay_circulator
    GPIO.setmode(GPIO.BOARD)
    relay_magstir = 40
    relay_main_pump = 38
    relay_circulator = 36
    GPIO.setup(relay_magstir, GPIO.OUT)
    GPIO.setup(relay_main_pump, GPIO.OUT)
    GPIO.setup(relay_circulator, GPIO.OUT)

#ascii value to a float
def ascii_to_float(an_ascii):
    a_string = ''.join([chr(value) for value in an_ascii])
    printable = set(string.printable)
    a_string = ''.join(filter(lambda x: x in printable, a_string))
    a_float = float(a_string)
    return a_float

#variable to a list of ascii values
def variable_to_ascii(a_var):
    a_string = str(a_var)
    a_list = list(a_string)
    l = len(a_list)
    for i in range(0,l):
        an_ascii_list = a_list
        an_ascii_list[i] = ord(a_list[i])
    return an_ascii_list

#read device from adress and convert to float
def device_read(i2c_address):
    bus.write_byte(i2c_address,82)
    time.sleep(0.9)
    x = bus.read_i2c_block_data(i2c_address,82)
    x_float = ascii_to_float(x)
    return x_float

#EC temperature compensation
def ec_temp_compensated(temp):
    cmd = variable_to_ascii(temp)
    cmd.insert(0,44)
    bus.write_i2c_block_data(ec_address,84,cmd)
    ec = device_read(ec_address)
    return ec

#read cycle on all sensors
def read_cycle():
    global temp
    global ph
    global ec
    temp = device_read(temp_address)
    ph = device_read(ph_address)
    ec = device_read(ec_address)
    return

#dosing pump command: D,[mL]
def dose_volume(i2c_address,volume):
    if volume < 0.5: volume = 0.5 #sets value to minimum dispense volume
    cmd = variable_to_ascii(volume)
    cmd.insert(0,44)
    bus.write_i2c_block_data(i2c_address,68,cmd)

#pre-dosing procedue
def dose_procedure():
    GPIO_configuration()
    os.system("xset dpms force on") #screen on
    GPIO.output(relay_main_pump, GPIO.HIGH) #reservoir main pump off
    GPIO.output(relay_circulator, GPIO.HIGH) #reservoir circulating pump on
    GPIO.output(relay_magstir, GPIO.HIGH) #magnetic stir cycle start
    time.sleep(3)
    GPIO.output(relay_magstir, GPIO.LOW)
    time.sleep(3)
    GPIO.output(relay_magstir, GPIO.HIGH)
    time.sleep(3)
    GPIO.output(relay_magstir, GPIO.LOW)

#restarts hydroponic main pumping system
def run_procedure():
    GPIO_configuration()
    GPIO.output(relay_main_pump, GPIO.LOW) #reservoir main pump on
    GPIO.output(relay_circulator, GPIO.LOW) #reservoir circulating pump off
    GPIO.cleanup()
    
#adjusts EC
def ec_adjust_cycle():
    dose_procedure()
    while True:
        read_cycle()
        error = ec_setpoint - ec
        value_readout()
        if error < 10:
            ec_pid.reset()
            break
        else:
            output_dose = ec_pid(ec)*0.03
            print('\nFert A&B: ' + '%.5s' % str(output_dose) + ' mL\n')
            dose_volume(doser1,output_dose)
            dose_volume(doser2,output_dose)
        time.sleep(120)

#adjusts pH up
def ph_adjust_up_cycle():
    dose_procedure()
    while True:
        read_cycle()
        error = ph_setpoint - ph
        value_readout()
        if error < 0.2:
            ph_pid.reset()
            break
        else:
            output_dose = ph_pid(ph)
            print('\npH up dose: ' + '%.5s' % str(output_dose) + '\n')
            dose_volume(doser3,output_dose)
        time.sleep(120)

#adjusts pH down
def ph_adjust_down_cycle():
    dose_procedure()
    while True:
        read_cycle()
        error =  ph - ph_setpoint
        value_readout()
        if error < 0.2:
            ph_pid.reset()
            break
        else:
            output_dose = ph_pid(ph)
            print('\npH down dose: ' + '%.5s' % str(output_dose) + '\n')
            dose_volume(doser3,output_dose)
        time.sleep(120)

#settings readout
def setting_readout():
    print('' + '\t' *1 + 'Setpoint:' + '\t' *1 + 'kp:' + '\t' *2 + 'ki:' + '\t' *2 + 'kd:' + '\t' *2 + 'Trigger:')
    print('EC: ' + '\t' *1 + str(ec_setpoint) + '\t' *2 + str(ec_kp) + '\t' *2 + str(ec_ki) + '\t' *2 + str(ec_kd) + '\t' *2 + str(ec_trigger))
    print('pH: ' + '\t' *1 + str(ph_setpoint) + '\t' *2 + str(ph_kp) + '\t' *2 + str(ph_ki) + '\t' *2 + str(ph_kd) + '\t' *2 + str(ph_trigger))

#value readout
def value_readout():
    print('EC:' + '\t' *2 + 'pH:' + '\t' *2 + 'Temperature (C):')
    print(str(ec) + '\t' *2 + str(ph) + '\t' *2 + str(temp))

#user setting input
def setting_input():
    while True:
        global ec_kp, ec_ki, ec_kd, ec_setpoint, ec_trigger, ph_kp, ph_ki, ph_kd, ph_setpoint, ph_trigger
        setting_readout()
        setting = input('Menu: EC, ph, pump, or start \n')
        if setting.lower() == 'ec':
            parameter = input('EC parameter: kp, ki, kd, set, trigger, or menu \n')
            if parameter.lower() == 'kp':
                ec_kp = float(input('EC kp: '))
                content[1] = str(ec_kp) + '\n'
            elif parameter.lower() == 'ki':
                ec_ki = float(input('EC ki: '))
                content[3] = str(ec_ki) + '\n'
            elif parameter.lower() == 'kd':
                ec_kd = float(input('EC kd: '))
                content[5] = str(ec_kd) + '\n'
            elif parameter.lower() == 'set':
                ec_setpoint = float(input('EC set point: '))
                content[7] = str(ec_setpoint) + '\n'
            elif setting.lower() == 'trigger':
                ec_trigger = float(input ('EC trigger deviation: '))
                content[9] = str(ec_trigger) + '\n'
        elif setting.lower() == 'ph':
            parameter = input('pH parameter: kp, ki, kd, set, trigger, or menu \n')
            if parameter.lower() == 'kp':
                ph_kp = float(input('pH kp: '))
                content[11] = str(ph_kp) + '\n'
            elif parameter.lower() == 'ki':
                ph_ki = float(input('pH ki: '))
                content[13] = str(ph_ki) + '\n'
            elif parameter.lower() == 'kd':
                ph_kd = float(input('pH kd: '))
                content[15] = str(ph_kd) + '\n'
            elif parameter.lower() == 'set':
                ph_setpoint = float(input('pH set point: '))
                content[17] = str(ph_setpoint) + '\n'
            elif setting.lower() == 'trigger':
                ph_trigger = float(input ('pH trigger deviation: '))
                content[19] = str(ph_trigger) + '\n'
        elif setting.lower() == 'pump':
            pump = input('Pump: A, B, up, down, or menu \n')
            if pump.lower() == 'a':
                dose = float(input ('Pump part A dose: '))
                dose_procedure()
                dose_volume(doser1,dose)
            elif pump.lower() == 'b':
                dose = float(input ('Pump part B dose: '))
                dose_procedure()
                dose_volume(doser2,dose)
            elif pump.lower() == 'up':
                dose = float(input ('Pump pH-up dose: '))
                dose_procedure()
                dose_volume(doser3,dose)
            elif pump.lower() == 'down':
                dose = float(input ('Pump pH-down dose: '))
                dose_procedure()
                dose_volume(doser4,dose)
        elif setting.lower() == 'start':
            break

#reads settings from file
file = open("hydro_settings.txt", "r")
content = file.readlines() # read the content of the file opened
ec_kp = float(content[1])
ec_ki = float(content[3])
ec_kd = float(content[5])
ec_setpoint = float(content[7])
ec_trigger = float(content[9])
ph_kp = float(content[11])
ph_ki = float(content[13])
ph_kd = float(content[15])
ph_setpoint = float(content[17])
ph_trigger = float(content[19])
file.close()

#call user setting input function
setting_input()

#writes settings to file
file = open("hydro_settings.txt","w")
file.writelines(content)
file.close()

#PID parameters
ec_pid = PID(ec_kp, ec_ki, ec_kd, setpoint = ec_setpoint)
ec_pid_sample_time = 120
ec_pid.sample_time = ec_pid_sample_time
ec_pid.proportional_on_measurement = True
ec_pid.output_limits = (0, 160)

ph_pid = PID(ph_kp, ph_ki, ph_kd, setpoint = ph_setpoint)
ph_pid_sample_time = 120
ph_pid.sample_time = ph_pid_sample_time
ph_pid.proportional_on_measurement = True
ph_pid.output_limits = (0, 80)

#measuring and dosing cycle
while True:
    read_cycle()
    value_readout()
    
    ec_adjust = False
    ph_adjust_up = False
    ph_adjust_down = False
    
    if ec < (ec_setpoint - ec_trigger): ec_adjust = True
    elif ph < (ph_setpoint - ph_trigger): ph_adjust_up = True
    elif ph > (ph_setpoint + ph_trigger): ph_adjust_down = True
    
    if ec_adjust == True:
        ec_adjust_cycle()
        ph_adjust_up_cycle()
        run_procedure()
                
    elif ec_adjust == False and ph_adjust_up == True:
        ph_adjust_up_cycle()
        run_procedure()
            
    elif ec_adjust == False and ph_adjust_down == True:
        ph_adjust_down_cycle()
        run_procedure()
 	   		
    		

Application

Grphical User Interface (GUI) Version

This version remains under development and is not yet entirely functional.

    		
import csv
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import os
import pandas as pd
import RPi.GPIO as GPIO
from simple_pid import PID
import smbus
import string
import sys
import threading
import time
import tkinter as tk
from tkinter import ttk
sys.path.insert(0,'/home/pi/.local/lib/python3.7/site-packages')


'''FILE & DATA HANDLING FUNCTIONS'''

#file processing
def x_axis_range():
	global x_axis_sec
	if x_axis == '6 hours': x_axis_sec = 21600
	elif x_axis == '12 hours': x_axis_sec = 43200
	elif x_axis == '24 hours': x_axis_sec = 86400
	elif x_axis == '48 hours': x_axis_sec = 172800
	else: x_axis_sec = 259200
	return

#reads settings from file
def settings_read():
	global content, ec_kp, ec_ki, ec_kd, ec_setpoint, ec_trigger, ph_kp, ph_ki, ph_kd, ph_setpoint, ph_trigger, vol_1, vol_2, vol_3, vol_4, reservoir_1, reservoir_2, reservoir_3, reservoir_4, ec, ph, temp, ec_pid_chk, ph_pid_chk, x_axis
	file = open('hydro_settings.txt', 'r')
	content = file.readlines() # read the content of the file opened
	ec_kp = float(content[1])
	ec_ki = float(content[3])
	ec_kd = float(content[5])
	ec_setpoint = float(content[7])
	ec_trigger = float(content[9])
	ph_kp = float(content[11])
	ph_ki = float(content[13])
	ph_kd = float(content[15])
	ph_setpoint = float(content[17])
	ph_trigger = float(content[19])
	vol_1 = float(content[21])
	vol_2 = float(content[23])
	vol_3 = float(content[25])
	vol_4 = float(content[27])
	reservoir_1 = float(content[29])
	reservoir_2 = float(content[31])
	reservoir_3 = float(content[33])
	reservoir_4 = float(content[35])
	ec = float(content[37])
	ph = float(content[39])
	temp = float(content[41])
	ec_pid_chk = int(content[43])
	ph_pid_chk = int(content[45])
	x_axis = str(content[47])
	file.close()
	return
def data_log_read():
    global data_list, data_list_resized
    # import full contents of csv file into list and remove header
    with open('hydro_data_log.csv', newline='') as file:
        reader = csv.reader(file)
        data_list = list(reader)
        data_list.pop(0)
        data_list_length = len(data_list)
        data_list_resized = []
    # resize list to 72 hours
    for i in range(data_list_length):
        difference = time.time() - float(data_list[i][0])
        if difference < x_axis_sec:
            data_list_resized.append(data_list[i])
    return

#writes settings to file
def settings_write():
	ec_setpoint = current_ec_setpoint.get()
	ph_setpoint = current_ph_setpoint.get()
	ec_pid_chk = current_ec_chk.get()
	ph_pid_chk = current_ph_chk.get()
	x_axis = current_x_axis.get()
	ec_kp = current_ec_kp.get()
	ec_ki = current_ec_ki.get()
	ec_kd = current_ec_kd.get()
	ph_kp = current_ph_kp.get()
	ph_ki = current_ph_ki.get()
	ph_kd = current_ph_kd.get()

	file = open('hydro_settings.txt', 'w')
	content[1] = str(ec_kp)+'\n'
	content[3] = str(ec_ki)+'\n'
	content[5] = str(ec_kd)+'\n'
	content[7] = str(ec_setpoint)+'\n'
	content[9] = str(ec_trigger)+'\n'
	content[11] = str(ph_kp)+'\n'
	content[13] = str(ph_ki)+'\n'
	content[15] = str(ph_kd)+'\n'
	content[17] = str(ph_setpoint)+'\n'
	content[19] = str(ph_trigger)+'\n'
	content[21] = str(vol_1)+'\n'
	content[23] = str(vol_2)+'\n'
	content[25] = str(vol_3)+'\n'
	content[27] = str(vol_4)+'\n'
	content[29] = str(reservoir_1)+'\n'
	content[31] = str(reservoir_2)+'\n'
	content[33] = str(reservoir_3)+'\n'
	content[35] = str(reservoir_4)+'\n'
	content[37] = str(ec)+'\n'
	content[39] = str(ph)+'\n'
	content[41] = str(temp)+'\n'
	content[43] = str(ec_pid_chk)+'\n'
	content[45] = str(ph_pid_chk)+'\n'
	content[47] = str(x_axis)
	file.writelines(content)
	file.close()
	return
def settings_write_with_event(event):
	x_axis_range()

	ec_setpoint = current_ec_setpoint.get()
	ph_setpoint = current_ph_setpoint.get()
	ec_pid_chk = current_ec_chk.get()
	ph_pid_chk = current_ph_chk.get()
	x_axis = current_x_axis.get()
	ec_kp = current_ec_kp.get()
	ec_ki = current_ec_ki.get()
	ec_kd = current_ec_kd.get()
	ph_kp = current_ph_kp.get()
	ph_ki = current_ph_ki.get()
	ph_kd = current_ph_kd.get()

	file = open('hydro_settings.txt', 'w')
	content[1] = str(ec_kp)+'\n'
	content[3] = str(ec_ki)+'\n'
	content[5] = str(ec_kd)+'\n'
	content[7] = str(ec_setpoint)+'\n'
	content[9] = str(ec_trigger)+'\n'
	content[11] = str(ph_kp)+'\n'
	content[13] = str(ph_ki)+'\n'
	content[15] = str(ph_kd)+'\n'
	content[17] = str(ph_setpoint)+'\n'
	content[19] = str(ph_trigger)+'\n'
	content[21] = str(vol_1)+'\n'
	content[23] = str(vol_2)+'\n'
	content[25] = str(vol_3)+'\n'
	content[27] = str(vol_4)+'\n'
	content[29] = str(reservoir_1)+'\n'
	content[31] = str(reservoir_2)+'\n'
	content[33] = str(reservoir_3)+'\n'
	content[35] = str(reservoir_4)+'\n'
	content[37] = str(ec)+'\n'
	content[39] = str(ph)+'\n'
	content[41] = str(temp)+'\n'
	content[43] = str(ec_pid_chk)+'\n'
	content[45] = str(ph_pid_chk)+'\n'
	content[47] = str(x_axis)
	file.writelines(content)
	file.close()
	return
'''def data_log_write():
	data = [[time.time(), ec, ph, temp]]
	with open('hydro_data_log.csv', 'w', encoding='UTF8', newline='') as f:
		writer = csv.writer(f)
		writer.writerows(data) #write multiple rows
	return
'''

settings_read()
x_axis_range()
data_log_read()
 
'''GUI VARIABLES & FUNCTIONS'''

#variables GUI
bg_color = '#000000'
widget_bg_color = '#1e1e1e'
widget_fg_color = '#ffffff'
ec_color = '#fdb813'
ph_color = '#0cb04b'
temp_color = '#ffffff'
parameters_color = '#a020f0'
box_border_color = '#ffffff'
dotted_line_color = '#ffffff'
right_pane_width = 200
left_pane_width = 600
section_height = 66
padding_right = 10

#frame function
def General_Box(root_frame, w, h, s, border_thickness, border_color):
	boxframe = tk.Frame(root_frame, width = w, height = h, bg = bg_color, highlightthickness = border_thickness, highlightbackground = border_color)
	boxframe.pack(side = s, fill = tk.BOTH, expand = True)
	boxframe.pack_propagate(False)
	return boxframe
def Initial_Box(root_frame, border_thickness, border_color):
	boxframe = General_Box(root_frame, 800, section_height, tk.TOP, border_thickness, border_color)
	return boxframe
def Initial_Box_4(root_frame, border_thickness, border_color):
	boxframe = General_Box(root_frame, 800, 102, tk.TOP, border_thickness, border_color)
	return boxframe
def Graph_Box(root_frame):
	boxframe = General_Box(root_frame, left_pane_width, section_height, tk.LEFT, 0, box_border_color)
	return boxframe
def Readout_Box(root_frame):
	boxframe = General_Box(root_frame, right_pane_width, section_height, tk.RIGHT, 0, box_border_color)
	return boxframe
def Section_Box_Med(root_frame):
	boxframe = General_Box(root_frame, right_pane_width, 60, tk.TOP, 0, box_border_color)
	return boxframe
def Section_Box_Small(root_frame):
	boxframe = General_Box(root_frame, right_pane_width, 64, tk.TOP, 0, box_border_color)
	return boxframe
def Section_Box_Small_Half(root_frame):
	boxframe = General_Box(root_frame, right_pane_width*0.5, 64, tk.RIGHT, 0, box_border_color)
	return boxframe
def Section_Box_XS(root_frame):
	boxframe = General_Box(root_frame, right_pane_width, 32, tk.TOP, 0, box_border_color)
	return boxframe
def Section_Box_XXS(root_frame):
	boxframe = General_Box(root_frame, right_pane_width, 120/5, tk.TOP, 0, box_border_color)
	return boxframe
def Section_Box_XXS_Half(root_frame, width):
	boxframe = General_Box(root_frame, width, 120/5, tk.LEFT, 0, box_border_color)
	return boxframe

#canvas functions
def Dotted_Border(root_frame, c):
	f = General_Box(root_frame, 800, 1, tk.TOP, 0, box_border_color)
	f.pack(fill=None, expand=False)
	canvas = tk.Canvas(f, width = 800, height = 1, bg = bg_color, highlightthickness = 0)
	canvas.create_line(0, 0, 800, 0, dash = (1,10), fill = c)
	canvas.pack(fill = tk.BOTH, expand = True)
	return canvas

#label widget functions
def Header_Label(root_frame, txt, text_color):
	label = tk.Label(root_frame, text = txt, font = ('Arial Bold', 14), fg = text_color, bg = bg_color)
	label.pack(side = tk.LEFT, anchor = tk.NW)
	return label
def Readout_Label(root_frame, variable, text_color):
	label = tk.Label(root_frame, text = str(variable), font = ('Arial Bold', 30), fg = text_color, bg = bg_color)
	label.pack(side = tk.TOP, anchor = tk.W, padx = padding_right)
	return label
def para_header_label(root_frame, variable):
	label = tk.Label(root_frame, text = str(variable), font = ('Arial Bold', 12), fg = parameters_color, bg = bg_color)
	label.pack(side = tk.TOP)
	return label
def para_label(root_frame, variable):
	label = tk.Label(root_frame, text = str(variable), font = ('Arial Bold', 10), fg = parameters_color, bg = bg_color)
	label.pack(side = tk.LEFT)
	return label
def para_vol_label(root_frame, variable1, variable2):
	label = tk.Label(root_frame, text = str(variable1)+'%  ('+str(variable2)+' mL)  ', font = ('Arial', 10), fg = vol1_color, bg = bg_color)
	label.pack(side = tk.LEFT)
	return label

#spin-box widget functions
def volume_spinbox(root_frame):
	spinbox = tk.Spinbox(root_frame, from_=0, to=10000, increment = 1, font = ('Arial', 12), bg = widget_bg_color, fg = widget_fg_color, width = 6, highlightthickness = 0, relief = tk.FLAT)
	spinbox.pack(side = tk.RIGHT)
	return spinbox

#checkbox widget functions
def parameter_checkbox(root_frame, chk_text, chk_var):
	checkbox = tk.Checkbutton(root_frame, onvalue = 1, offvalue = 0, variable = chk_var, command = settings_write, text = str(chk_text), font = ('Arial Bold', 10), borderwidth = 0, bg = bg_color, fg = parameters_color, selectcolor = widget_bg_color, highlightbackground = bg_color)
	checkbox.pack(side = tk.TOP, anchor = tk.NW)
	return checkbox

#reservoir volume label color
def volume_color():
	def volume_color_set(volume_value):
		if volume_value >= 40: vol_color = '#0cb04b'
		if volume_value < 40 and volume_value > 10: vol_color = '#fdb813'
		if volume_value <= 10: vol_color = '#f05334'
		return vol_color
	global vol1_color, vol2_color, vol3_color, vol4_color
	vol1_color = volume_color_set(vol1_percent)
	vol2_color = volume_color_set(vol2_percent)
	vol3_color = volume_color_set(vol3_percent)
	vol4_color = volume_color_set(vol4_percent)
	return

#graph functions
def Create_Graph(root_frame, use_colmn, _color):
	if use_colmn == 'ec': use_col_num = 1
	elif use_colmn == 'ph': use_col_num = 2
	else: use_col_num = 3
	used_col = [sub[use_col_num] for sub in data_list_resized]
	used_col_int = list(map(int, used_col))
	data = pd.DataFrame({use_colmn:used_col_int})
	df = pd.DataFrame(data)

	#figure = plt.Figure(figsize=(left_pane_width, section_height), dpi=100)
	figure, ax = plt.subplots()
	canvas = FigureCanvasTkAgg(figure, root_frame)
	canvas.draw()
	canvas.get_tk_widget().pack(expand=True, fill=tk.BOTH)
	df.plot(kind = 'line', ax = ax, legend = False, color = _color, fontsize = 8, antialiased = True, linewidth = 1)

	figure.patch.set_facecolor(bg_color)
	ax.get_xaxis().set_ticks([])
	ax.spines['left'].set_color(_color)
	ax.spines['top'].set_visible(False)
	ax.spines['right'].set_visible(False)
	ax.spines['bottom'].set_visible(False)
	ax.tick_params(axis='y', color = _color, labelcolor = _color)
	ax.set_facecolor(bg_color)
	return

#button functions
def add_vol_button(root_frame):
	button = tk.Button(root_frame, text = 'ADD', font = ('Arial', 8), borderwidth = 0, fg = 'white', bg = widget_bg_color, activebackground = parameters_color, highlightbackground = bg_color)
	button.pack(side = tk.LEFT)
	return

'''CONTROL VARIABLES & FUNCTIONS'''

#GPIO configuration
def GPIO_configuration():
	global relay_magstir,relay_main_pump,relay_circulator
	GPIO.setmode(GPIO.BOARD)
	relay_magstir = 40
	relay_main_pump = 38
	relay_circulator = 36
	GPIO.setup(relay_magstir, GPIO.OUT)
	GPIO.setup(relay_main_pump, GPIO.OUT)
	GPIO.setup(relay_circulator, GPIO.OUT)
	return

#ascii value to a float
def ascii_to_float(an_ascii):
	a_string = ''.join([chr(value) for value in an_ascii])
	printable = set(string.printable)
	a_string = ''.join(filter(lambda x: x in printable, a_string))
	a_float = float(a_string)
	return a_float

#variable to a list of ascii values
def variable_to_ascii(a_var):
	a_string = str(a_var)
	a_list = list(a_string)
	l = len(a_list)
	for i in range(0,l):
		an_ascii_list = a_list
		an_ascii_list[i] = ord(a_list[i])
	return an_ascii_list

#read device from adress and convert to float
def device_read(i2c_address):
	bus.write_byte(i2c_address,82)
	time.sleep(0.9)
	x = bus.read_i2c_block_data(i2c_address,82)
	x_float = ascii_to_float(x)
	return x_float

#EC temperature compensation
def ec_temp_compensated(temp):
	cmd = variable_to_ascii(temp)
	cmd.insert(0,44)
	bus.write_i2c_block_data(ec_address,84,cmd)
	ec = device_read(ec_address)
	return ec

#read cycle on all sensors
def read_cycle():
	global temp
	global ph
	global ec
	temp = device_read(temp_address)
	ph = device_read(ph_address)
	ec = device_read(ec_address)
	return

#dosing pump command: D,[mL]
def dose_volume(i2c_address,volume):
	if volume < 0.5: volume = 0.5 #sets value to minimum dispense volume
	cmd = variable_to_ascii(volume)
	cmd.insert(0,44)
	bus.write_i2c_block_data(i2c_address,68,cmd)

#pre-dosing procedure with pump reverse/bubbling
def dose_procedure():
	GPIO_configuration()
	os.system("xset dpms force on") #screen on
	GPIO.output(relay_main_pump, GPIO.HIGH) #reservoir main pump off
	GPIO.output(relay_circulator, GPIO.HIGH) #reservoir circulating pump on
	cmd = variable_to_ascii(-abs(tube_volume)) #sets tube volume negative for reverse pump
	cmd.insert(0,44)
	bus.write_i2c_block_data(doser1,68,cmd)
	bus.write_i2c_block_data(doser2,68,cmd)
	bus.write_i2c_block_data(doser3,68,cmd)
	bus.write_i2c_block_data(doser4,68,cmd)

#restarts hydroponic main pumping system
def run_procedure():
	GPIO_configuration()
	GPIO.output(relay_main_pump, GPIO.LOW) #reservoir main pump on
	GPIO.output(relay_circulator, GPIO.LOW) #reservoir circulating pump off
	GPIO.cleanup()

#adjusts EC
def ec_adjust_cycle():
	dose_procedure()
	while True:
		read_cycle()
		error = ec_setpoint - ec
		if error < 10:
			ec_pid.reset()
			break
		else:
			output_dose = ec_pid(ec)*0.03
			print('\nFert A&B: ' + '%.5s' % str(output_dose) + ' mL\n')
			dose_volume(doser1,output_dose)
			dose_volume(doser2,output_dose)
		time.sleep(120)

#adjusts pH up
def ph_adjust_up_cycle():
	dose_procedure()
	while True:
		read_cycle()
		error = ph_setpoint - ph
		if error < 0.2:
			ph_pid.reset()
			break
		else:
			output_dose = ph_pid(ph)
			print('\npH up dose: ' + '%.5s' % str(output_dose) + '\n')
			dose_volume(doser3,output_dose)
		time.sleep(120)

#adjusts pH down
def ph_adjust_down_cycle():
	dose_procedure()
	while True:
		read_cycle()
		error =  ph - ph_setpoint
		if error < 0.2:
			ph_pid.reset()
			break
		else:
			output_dose = ph_pid(ph)
			print('\npH down dose: ' + '%.5s' % str(output_dose) + '\n')
			dose_volume(doser3,output_dose)
		time.sleep(120)

#reservoir volume percent
def volume_percent():
	global vol1_percent, vol2_percent, vol3_percent, vol4_percent
	vol1_percent = vol_1/reservoir_1
	vol2_percent = vol_2/reservoir_2
	vol3_percent = vol_3/reservoir_3
	vol4_percent = vol_4/reservoir_4
	return

volume_percent()
volume_color()


def control():

	#initialize variables
	global tube_volume
	tube_volume = 5 #peristaltic pump tube volume

	#initialize I2C bus and addresses using hexidecimal
	global bus, ph_address, ec_address, temp_address, doser1, doser2, doser3, doser4
	bus = smbus.SMBus(1)
	ph_address = 0x63
	ec_address = 0x64
	temp_address = 0x66
	doser1 = 0xA
	doser2 = 0xB
	doser3 = 0xC
	doser4 = 0xD

	#PID parameters
	ec_pid = PID(ec_kp, ec_ki, ec_kd, setpoint = ec_setpoint)
	ec_pid_sample_time = 120
	ec_pid.sample_time = ec_pid_sample_time
	ec_pid.proportional_on_measurement = True
	ec_pid.output_limits = (0, 160)

	ph_pid = PID(ph_kp, ph_ki, ph_kd, setpoint = ph_setpoint)
	ph_pid_sample_time = 120
	ph_pid.sample_time = ph_pid_sample_time
	ph_pid.proportional_on_measurement = True
	ph_pid.output_limits = (0, 80)

	#running - measuring and dosing
	while True:
		read_cycle()
		volume_percent()
		volume_color()
	
		ec_adjust = False
		ph_adjust_up = False
		ph_adjust_down = False
	
		if ec < (ec_setpoint - ec_trigger): ec_adjust = True
		elif ph < (ph_setpoint - ph_trigger): ph_adjust_up = True
		elif ph > (ph_setpoint + ph_trigger): ph_adjust_down = True
	
		if ec_adjust == True:
			ec_adjust_cycle()
			ph_adjust_up_cycle()
			run_procedure()
				
		elif ec_adjust == False and ph_adjust_up == True:
			ph_adjust_up_cycle()
			run_procedure()
			
		elif ec_adjust == False and ph_adjust_down == True:
			ph_adjust_down_cycle()
			run_procedure()
	return
def GUI():

	# window from tkinter package
	window = tk.Tk()
	window.title('HYDRO CONTROL')
	window.geometry('800x400+0+0')
	#window.attributes('-fullscreen',True)

	# root frame
	frame = tk.Frame(window)
	frame.pack(fill = tk.BOTH, expand = True)

	# frame structure
	box1 = Initial_Box(frame, 0, '#000000')
	border1 = Dotted_Border(frame, dotted_line_color)
	box2 = Initial_Box(frame, 0, '#000000')
	border2 = Dotted_Border(frame, dotted_line_color)
	box3 = Initial_Box(frame, 0, '#000000')
	border3 = Dotted_Border(frame, dotted_line_color)
	box4 = Initial_Box_4(frame, 0, '#000000')
	border4 = Dotted_Border(frame, '#000000')

	box1_L = Graph_Box(box1)
	box2_L = Graph_Box(box2)
	box3_L = Graph_Box(box3)

	box1_R = Readout_Box(box1)
	box2_R = Readout_Box(box2)
	box3_R = Readout_Box(box3)

	box1_R_Top = Section_Box_XS(box1_R)
	box2_R_Top = Section_Box_XS(box2_R)
	box3_R_Top = Section_Box_XS(box3_R)

	box1_R_Bottom = Section_Box_Small(box1_R)
	box1_R_Bottom_R = Section_Box_Small_Half(box1_R_Bottom)
	box2_R_Bottom = Section_Box_Small(box2_R)
	box2_R_Bottom_R = Section_Box_Small_Half(box2_R_Bottom)
	box3_R_Bottom = Section_Box_Small(box3_R)

	box4_R_2 = Readout_Box(box4)
	box4_R_1 = Readout_Box(box4)
	box4_L_2 = Readout_Box(box4)
	box4_L_1 = Readout_Box(box4)

	box4_L_1_Top = Section_Box_XXS(box4_L_1)
	box4_L_1_Mid1 = Section_Box_XXS(box4_L_1)
	box4_L_1_Mid2 = Section_Box_XXS(box4_L_1)
	box4_L_1_Mid3 = Section_Box_XXS(box4_L_1)
	box4_L_1_Bottom = Section_Box_XXS(box4_L_1)

	box4_L_2_Top = Section_Box_Med(box4_L_2)
	box4_L_2_Bottom = Section_Box_Med(box4_L_2)

	box4_R_1_Top = Section_Box_XXS(box4_R_1)
	box4_R_1_Mid1 = Section_Box_XXS(box4_R_1)
	box4_R_1_Mid2 = Section_Box_XXS(box4_R_1)
	box4_R_1_Mid3 = Section_Box_XXS(box4_R_1)
	box4_R_1_Bottom = Section_Box_XXS(box4_R_1)

	box4_R_2_Top = Section_Box_XXS(box4_R_2)
	box4_R_2_Mid1 = Section_Box_XXS(box4_R_2)
	box4_R_2_Mid2 = Section_Box_XXS(box4_R_2)
	box4_R_2_Mid3 = Section_Box_XXS(box4_R_2)
	box4_R_2_Bottom = Section_Box_XXS(box4_R_2)

	box4_L_1_Mid1_L = Section_Box_XXS_Half(box4_L_1_Mid1, 100)
	box4_L_1_Mid1_R = Section_Box_XXS_Half(box4_L_1_Mid1, 100)
	box4_L_1_Mid2_L = Section_Box_XXS_Half(box4_L_1_Mid2, 70)
	box4_L_1_Mid2_R = Section_Box_XXS_Half(box4_L_1_Mid2, 130)
	box4_L_1_Mid3_L = Section_Box_XXS_Half(box4_L_1_Mid3, 100)
	box4_L_1_Mid3_R = Section_Box_XXS_Half(box4_L_1_Mid3, 100)
	box4_L_1_Bottom_L = Section_Box_XXS_Half(box4_L_1_Bottom, 100)
	box4_L_1_Bottom_R = Section_Box_XXS_Half(box4_L_1_Bottom, 100)

	box4_R_1_Mid1_L = Section_Box_XXS_Half(box4_R_1_Mid1, 70)
	box4_R_1_Mid1_R = Section_Box_XXS_Half(box4_R_1_Mid1, 130)
	box4_R_1_Mid2_L = Section_Box_XXS_Half(box4_R_1_Mid2, 70)
	box4_R_1_Mid2_R = Section_Box_XXS_Half(box4_R_1_Mid2, 130)
	box4_R_1_Mid3_L = Section_Box_XXS_Half(box4_R_1_Mid3, 70)
	box4_R_1_Mid3_R = Section_Box_XXS_Half(box4_R_1_Mid3, 130)
	box4_R_1_Bottom_L = Section_Box_XXS_Half(box4_R_1_Bottom, 70)
	box4_R_1_Bottom_R = Section_Box_XXS_Half(box4_R_1_Bottom, 130)

	box4_R_2_Mid1_L = Section_Box_XXS_Half(box4_R_2_Mid1, 100)
	box4_R_2_Mid1_R = Section_Box_XXS_Half(box4_R_2_Mid1, 100)
	box4_R_2_Mid2_L = Section_Box_XXS_Half(box4_R_2_Mid2, 100)
	box4_R_2_Mid2_R = Section_Box_XXS_Half(box4_R_2_Mid2, 100)
	box4_R_2_Mid3_L = Section_Box_XXS_Half(box4_R_2_Mid3, 100)
	box4_R_2_Mid3_R = Section_Box_XXS_Half(box4_R_2_Mid3, 100)
	box4_R_2_Bottom_L = Section_Box_XXS_Half(box4_R_2_Bottom, 100)
	box4_R_2_Bottom_R = Section_Box_XXS_Half(box4_R_2_Bottom, 100)

	# checkbuttons
	global current_ec_chk, current_ph_chk
	current_ec_chk = tk.IntVar(value = ec_pid_chk)
	current_ph_chk = tk.IntVar(value = ph_pid_chk)
	
	chk_pid_ec = parameter_checkbox(box4_L_2_Top, 'EC PID | COEFFICIENTS:', current_ec_chk)
	chk_pid_ph = parameter_checkbox(box4_L_2_Bottom, 'pH PID | COEFFICIENTS:', current_ph_chk)

	# reservoir volume label color
	volume_color()

	# labels
	ec_lbl_header = Header_Label(box1_R_Top, 'EC/TDS (μS/cm)', ec_color)
	ph_lbl_header = Header_Label(box2_R_Top, 'Acid/Base (pH)', ph_color)
	temp_lbl_header = Header_Label(box3_R_Top, 'Temperature (℉)', temp_color)
	parameters_lbl_header = Header_Label(box4_L_1_Top, ' Parameters ⚛', parameters_color).pack(side = tk.TOP, anchor = tk.NW)

	ec_lbl_readout = Readout_Label(box1_R_Bottom, "{:.2f}".format(ec), ec_color)
	ph_lbl_readout = Readout_Label(box2_R_Bottom, "{:.2f}".format(ph), ph_color)
	temp_lbl_readout = Readout_Label(box3_R_Bottom, "{:.1f}".format(temp), temp_color)

	para_xaxis_lbl_header = tk.Label(box4_L_1_Mid2_L, text = '   X AXES:', font = ('Arial Bold', 10), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)

	para_vol_lbl_header = para_header_label(box4_R_1_Top, 'REMAINING VOLUMES')
	para_vol_lbl_1 = para_label(box4_R_1_Mid1_L, 'PART A')
	para_vol_lbl_1_p = para_vol_label(box4_R_1_Mid1_R, vol1_percent, vol_1)
	para_vol_lbl_2 = para_label(box4_R_1_Mid2_L, 'PART B')
	para_vol_lbl_2_p = para_vol_label(box4_R_1_Mid2_R, vol2_percent, vol_2)
	para_vol_lbl_3 = para_label(box4_R_1_Mid3_L, 'pH UP')
	para_vol_lbl_3_p = para_vol_label(box4_R_1_Mid3_R, vol3_percent, vol_3)
	para_vol_lbl_4 = para_label(box4_R_1_Bottom_L, 'pH DOWN')
	para_vol_lbl_4_p = para_vol_label(box4_R_1_Bottom_R, vol4_percent, vol_4)
	para_vol_lbl_header = para_header_label(box4_R_2_Top,'ADD VOLUME (mL)')

	# buttons
	vol1_reset = add_vol_button(box4_R_2_Mid1_R)
	vol2_reset = add_vol_button(box4_R_2_Mid2_R)
	vol3_reset = add_vol_button(box4_R_2_Mid3_R)
	vol4_reset = add_vol_button(box4_R_2_Bottom_R)

	# spin-boxes
	global current_ec_setpoint, current_ph_setpoint
	current_ec_setpoint = tk.DoubleVar(value = ec_setpoint)
	current_ph_setpoint = tk.DoubleVar(value = ph_setpoint)
	
	ec_input = tk.Spinbox(box1_R_Bottom_R, from_=0, to=2, increment = 0.1, wrap = False, font = ('Arial', 18), bg = bg_color, fg = ec_color, width = 3, highlightthickness = 0, relief = tk.FLAT, textvariable = current_ec_setpoint, command = settings_write)
	ec_input.pack(side = tk.RIGHT, anchor = tk.W, padx = padding_right+4)

	ph_input = tk.Spinbox(box2_R_Bottom_R, from_=4, to=8, increment = 0.1, wrap = False, font = ('Arial', 18), bg = bg_color, fg = ph_color, width = 3, highlightthickness = 0, relief = tk.FLAT, textvariable = current_ph_setpoint, command = settings_write)
	ph_input.pack(side = tk.RIGHT, anchor = tk.W, padx = padding_right+4)

	vol1_input = volume_spinbox(box4_R_2_Mid1_L)
	vol2_input = volume_spinbox(box4_R_2_Mid2_L)
	vol3_input = volume_spinbox(box4_R_2_Mid3_L)
	vol4_input = volume_spinbox(box4_R_2_Bottom_L)

	# Define the style for combobox widget
	window.option_add("*TCombobox*Listbox*Background", widget_bg_color)
	window.option_add('*TCombobox*Listbox*Foreground', widget_fg_color)
#	window.option_add('*TCombobox*Listbox*selectBackground', widget_bg_color)
#	window.option_add('*TCombobox*Listbox*selectForeground', widget_fg_color)
	style= ttk.Style()
	style.map('TCombobox', fieldbackground=[('readonly', widget_bg_color)])
	style.map('TCombobox', selectbackground=[('readonly', widget_bg_color)])
	style.map('TCombobox', selectforeground=[('readonly', widget_fg_color)])
	style.map('TCombobox', background=[('readonly', widget_bg_color)])
	style.map('TCombobox', foreground=[('readonly', widget_fg_color)])
	style.map('TCombobox', bordercolor=[('readonly', widget_bg_color)])
	style.map('TCombobox', arrowcolor=[('readonly', widget_fg_color)])

	# comboboxes
	global current_x_axis
	current_x_axis = tk.StringVar(value = x_axis)

	combobox1 = ttk.Combobox(box4_L_1_Mid2_R, textvariable = current_x_axis, width = 7)
	combobox1.bind('<>', settings_write_with_event)
	combobox1['state'] = 'readonly'
	combobox1['values'] = ('6 hours', '12 hours', '24 hours', '48 hours', '72 hours')
	combobox1.pack(side = tk.LEFT)

	# entries
	global current_ec_kp, current_ec_ki, current_ec_kd, current_ph_kp, current_ph_ki, current_ph_kd
	current_ec_kp = tk.StringVar(value = ec_kp)
	current_ec_ki = tk.StringVar(value = ec_ki)
	current_ec_kd = tk.StringVar(value = ec_kd)
	current_ph_kp = tk.StringVar(value = ph_kp)
	current_ph_ki = tk.StringVar(value = ph_ki)
	current_ph_kd = tk.StringVar(value = ph_kd)

	ec_kp_lbl = tk.Label(box4_L_2_Top, text = 'p ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ec_kp_input = tk.Entry(box4_L_2_Top, textvariable = current_ec_kp, width = 4, bg = widget_bg_color, fg = ec_color, highlightthickness = 0)
	ec_kp_input.pack(side = tk.LEFT)
	ec_kp_input.bind("", settings_write_with_event)  

	ec_ki_lbl = tk.Label(box4_L_2_Top, text = '  i ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ec_ki_input = tk.Entry(box4_L_2_Top, textvariable = current_ec_ki, width = 4, bg = widget_bg_color, fg = ec_color, highlightthickness = 0)
	ec_ki_input.pack(side = tk.LEFT)
	ec_kp_input.bind("", settings_write_with_event)

	ec_kd_lbl = tk.Label(box4_L_2_Top, text = '  d ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ec_kd_input = tk.Entry(box4_L_2_Top, textvariable = current_ec_kd, width = 4, bg = widget_bg_color, fg = ec_color, highlightthickness = 0)
	ec_kd_input.pack(side = tk.LEFT)
	ec_kp_input.bind("", settings_write_with_event)

	ph_kp_lbl = tk.Label(box4_L_2_Bottom, text = 'p ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ph_kp_input = tk.Entry(box4_L_2_Bottom, textvariable = current_ph_kp, width = 4, bg = widget_bg_color, fg = ph_color, highlightthickness = 0)
	ph_kp_input.pack(side = tk.LEFT)
	ph_kp_input.bind("", settings_write_with_event)  

	ph_ki_lbl = tk.Label(box4_L_2_Bottom, text = '  i ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ph_ki_input = tk.Entry(box4_L_2_Bottom, textvariable = current_ph_ki, width = 4, bg = widget_bg_color, fg = ph_color, highlightthickness = 0)
	ph_ki_input.pack(side = tk.LEFT)
	ph_kp_input.bind("", settings_write_with_event)

	ph_kd_lbl = tk.Label(box4_L_2_Bottom, text = '  d ', font = ('Arial', 12), fg = parameters_color, bg = bg_color).pack(side = tk.LEFT)
	ph_kd_input = tk.Entry(box4_L_2_Bottom, textvariable = current_ph_kd, width = 4, bg = widget_bg_color, fg = ph_color, highlightthickness = 0)
	ph_kd_input.pack(side = tk.LEFT)
	ph_kp_input.bind("", settings_write_with_event)

	# graphs
	Graph1 = Create_Graph(box1_L, 'ec', ec_color)
	Graph2 = Create_Graph(box2_L, 'ph', ph_color)
	Graph3 = Create_Graph(box3_L, 'temp', temp_color)

	window.mainloop()
	return


threading.Thread(target=control, daemon=True).start()
threading.Thread(target=GUI, daemon=False).start()
 	   		
    		

Hardware

  • Computer: Raspberry Pi 4
  • Stackable Add-on: Tentacle T3 [Whitebox Labs]
  • EC Sensor: Conductivity Probe K 0.1 [Atlas Scientific]
  • pH Sensor: Consumer Grade pH Probe [Atlas Scientific]
  • Temperature Sensor: PT-1000 Probe [Atlas Scientific]
  • Real-Time-Clock (RTC): PiRTC Precise DS3231 RTC [Adafruit]
  • Pumps: EZO-PMP Embedded Dosing Pump [Atlas Scientific]
  • Relays: 5v Relay - 1 Channel - Opto-Isolated High or Low Level Trigger
  • Screen: 7” touchscreen [Raspberry Pi]

Resources

[datasheet] EZO Conductivity Circuit.pdf

[datasheet] EZO pH Circuit.pdf

[datasheet] EZO PMP Peristaltic Pump.pdf

[datasheet] EZO RTD Temperature Circuit.pdf

Created 02/17/25 | Modified 05/22/25