Software & Algorithm
User Interface:
For a successful product, it is crucial to have an intuitive user interface. Our solution is a grid with the name of the spices above drop-down options. The drop-down options are in increments of 5mL. Additionally, the bottom row includes several preset recipes. After selection, the user would hit submit for the dispenser to start.
GUI Code
from tkinter import *
#ser = serial.Serial('COM3', 19200)
measurementsNew = []
class SpiceGrid:
def __init__(self, master):
self.master = master
master.title("Spice Grid")
# Create the grid
self.grid = [[None for _ in range(3)] for _ in range(3)]
spices = ["Paprika", "Salt", "Pepper", "Turmeric", "Himal. Salt", "Oregano", "Chili Powder", "Cumin", "HOME"]
count = 0
# Create the labels and dropdown menus
for i in range(3):
for j in range(3):
frame = Frame(master, width=150, height=150)
frame.grid(row=i, column=j, padx=10, pady=10)
label = Label(frame, text=spices[count], font=("Arial", 12))
label.pack()
count = count + 1
self.grid[i][j] = StringVar(master)
self.grid[i][j].set("0 mL")
if(i != 2 or j != 2):
dropdown = OptionMenu(frame, self.grid[i][j], "0 mL", "5 mL", "10 mL", "15 mL", "20 mL")
dropdown.pack()
#spice_image = PhotoImage(file="p1.png")
#image_label = Label(frame, image=spice_image)
#image_label.pack()
# Add the recipe buttons
self.recipe_button_1 = Button(master, text="Recipe 1", command=lambda: self.load_recipe([0,5,10,5,0,5,0,5,0]))
self.recipe_button_1.grid(row=4, column=0, padx=10, pady=10)
self.recipe_button_2 = Button(master, text="Recipe 2", command=lambda: self.load_recipe([20,20,20,20,0,0,0,0,0]))
self.recipe_button_2.grid(row=4, column=1, padx=10, pady=10)
# Add the reset button
self.reset_button = Button(master, text="Reset", command=self.reset)
self.reset_button.grid(row=4, column=2, padx=10, pady=10)
# Add the submit button
self.submit_button = Button(master, text="Submit", command=self.submit)
self.submit_button.grid(row=5, column=1, pady=10)
def load_recipe(self, recipe):
# Load the specified recipe into the grid
for i in range(3):
for j in range(3):
self.grid[i][j].set(str(recipe[i*3+j]) + " mL")
def reset(self):
# Reset all dropdown menus to 0 mL
for i in range(3):
for j in range(3):
self.grid[i][j].set("0 mL")
def submit(self):
# Create a list of the selected measurements
global measurementsNew
measurements = []
for i in range(3):
for j in range(3):
measurement_str = self.grid[i][j].get()
measurement = int(measurement_str[:-3]) if measurement_str != "0 mL" else 0
measurements.append(measurement)
# Print the list of measurements
measurementsNew = measurements
# print(measurementsNew)
self.master.quit()
#ser.write(measurements)
# if __name__ == '__main__':
# root = Tk()
# spice_grid = SpiceGrid(root)
# root.mainloop()
def run_gui():
root = Tk()
spice_grid = SpiceGrid(root)
root.mainloop()
return measurementsNew
Data Transmission:
In order for a streamlined experience, it would be crucial to require the wireless transmission of the GUI selections to the dispenser. This would be accomplished by the user sending the data to the ESP32 through Bluetooth. Afterward, the ESP32 would send the G-Code commands through serial to the Arduino UNO.
Gantry Actuation:
We have two basic types of movements to consider for the gantry: Traversing and Dispensing
Traversing Methods
The gantry system is controlled via G-Code and only moves via X and Y coordinates
To traverse through relevant points in the X-Direction:
def nxt_col(col): # Move to col number
if col == 3:
movex = "X" + str(-1*offsetx)+"\n" # lower bound
elif col == 2:
movex = "X" + str(-1*((xlimit-offsetx)/2 + offsetx))+"\n" # right in the middle of the 2
elif col == 1:
movex = "X" + str(-1*(xlimit))+"\n" # upper bound
elif col == 0:
movex = "X" + str(0) + "\n"
print(movex)
S.write(bytes(movex, 'UTF-8'))To traverse through relevant points in the Y-Direction:
def nxt_row(row): # Move to row num
if row == 1:
movey = "Y" + str(-1*(ylimit)) + "\n" # upper bound
elif row == 2:
movey = "Y" + str(-1*((ylimit-offsety)/2 + offsety)) + "\n" # right in the middle of the 2 zones where they will not crash
elif row == 3:
movey = "Y" + str(-1*offsety) + "\n" # lower bound
elif row == 0:
movey = "Y" + str(0) + "\n"
print(movey)
S.write(bytes(movey, 'UTF-8'))In order to go to specified region numbers for the algorithm we utilize the methods we previously wrote:
def to_region(region_no): # Go to Start of Region Number
row = 3 # enter region between 1 and the others
if region_no in [1, 4]:
col = 3 # move up so moving up or sideways will result in no crashing
elif region_no == 2:
col = 1 # move left to region 2
elif region_no == 3:
col = 2 # In the middle of the limit and the x-offset
elif region_no == 5:
row = 1 # top row
col = 3
elif region_no == 6:
row = 2
col = 3
elif region_no == 0:
row = 0
col = 0
if region_no in [2, 3, 4]: # change row/col first depending on region
print("to region 2/3/4")
nxt_col(col)
nxt_row(row)
else:
print("to region 1/5/6")
nxt_row(row)
nxt_col(col)To enter or exit the space where we can actuate the dispenser:
def entr2disp(region_no, enter): # Enter/exit Dispensing Zone
starty = offsety #determine starting pos of y based on the region
if region_no in [1, 4, 5, 6]:
startx = offsetx #the starting positions are based on the to_region method
if region_no == 5:
starty = ylimit
elif region_no == 6:
starty = (ylimit-offsety)/2 + offsety
elif region_no == 2:
startx = xlimit
elif region_no == 3:
startx = (xlimit-offsetx)/2 + offsetx
# Entering Region
if enter == True:
if region_no in [2, 3, 4]:
movex = "X" + str(-1*(startx - 2.75)) + "\n" # calculate where to move to enter the region
S.write(bytes(movex, 'UTF-8')) # move right to enter the dispensing region
elif region_no in [1, 5, 6]:
movey = "Y" + str(-1*(starty - 6)) + "\n"
S.write(bytes(movey, 'UTF-8')) # move down to enter the dispensing region
else:
if region_no in [2, 3, 4]:
movex = "X" + str(-1*(startx)) + "\n"
S.write(bytes(movex, 'UTF-8'))
elif region_no in [1, 5, 6]:
movey = "Y" + str(-1*(starty)) + "\n"
S.write(bytes(movey, 'UTF-8')) Dispensing Methods
In order to actuate the dispenser, we call the basic traversing methods.
To dispense, I created a method that would handle the basic movements involved in dispensing to avoid rewriting the same lines:
def dispense(region_no, vertmove, moveto): # Enter Dispensing Zone, Dispense, Exit Zone
entr2disp(region_no, True)
if vertmove == True:
nxt_row(moveto) # Travel Bottom to Top
else:
nxt_col(moveto) # Travel Right to Left
entr2disp(region_no, False)To complete the dispense, we need to have a method to handle going to the starting point of the dispense, dispensing either one or multiple dispensers. Furthermore, we need to keep track of how much has been dispensed:
def dispReg(region_no, dual, further): # Handles Double or Single Dispense based off Position
# Regions 2, 3, 4 have the same dispensing movements
# Regions 1, 5, 6 have the same dispensing movements
global quantities
to_region(region_no)
if region_no in [2, 3, 4]:
topidx = region_no - 2
botidx = region_no + 1
if dual == True:
# Go to Region Start, Dispense Both, Stop
to_region(region_no)
dispense(region_no, True, 1) # Region Number, Vertical Movement, Row 1
quantities[topidx] -= 1
quantities[botidx] -= 1
elif dual == False and further == True: # Top Dispenser
# Top Dispense happens either solo or after a double
# Go to Top Row Start, Dispense, Stop
# Starting Point of Top Dispenser
nxt_col(region_no-1)
nxt_row(2)
dispense(region_no, True, 1) # Region Number, Vertical Movement, Row 1
quantities[topidx] -= 1
elif dual == False and further == False: # Bottom Dispenser
# Bottom Dispense happens either solo or after a double
# Go to Region Start, Dispense, Stop
to_region(region_no)
dispense(region_no, True, 2) # Region Number, Vertical Movement, Row 2
quantities[botidx] -= 1
elif region_no in [1, 5, 6]:
if region_no == 1:
leftidx = 6
rightidx = 7
elif region_no == 5:
leftidx = 0
rightidx = 1
else:
leftidx = 3
rightidx = 4
if dual == True:
# Go to Region Start, Dispense Both, Stop
to_region(region_no)
dispense(region_no, False, 1) # Region Number, NOT Vertical Movement, Col 1
quantities[leftidx] -= 1
quantities[rightidx] -= 1
elif dual == False and further == True: # Left Dispenser
# Left Dispense happens either solo or after a double
# Move to Left Col Start, Dispense
# Starting Point of Left Dispenser
nxt_row(abs(region_no-4)) # abs(5 - 4) = row 1, abs(6 - 4) = row 2, abs(1 - 4) = row 3
nxt_col(2)
dispense(region_no, False, 1) # Region Number, NOT Vertical Movement, Col 1
quantities[leftidx] -= 1
elif dual == False and further == False: # Right Dispenser
# Right Dispense happens either solo or after a double
# Go to Region Start, Dispense, Stop
to_region(region_no)
dispense(region_no, False, 2) # Region Number, NOT Vertical Movement, Col 2
quantities[rightidx] -= 1Dispensing Optimization:
Before we make our first move, we need to determine the best way to dispense the selected spices. In order to minimize moves, we look to the regions that have the potential to dispense multiple at once. Furthermore, we compare regions that overlap to see which direction is fastest.
Region Pairs Code
def regionPairs(quan): # Determine Amount of Pairs in each Region
global regIdx
regPairs = [0, 0, 0, 0, 0, 0]
for i in range(6):
quanCopy = quan
disp1 = quanCopy[regIdx[i][0]]
disp2 = quanCopy[regIdx[i][1]]
while disp1 > 0 and disp2 > 0:
disp1 -= 1
disp2 -= 1
if disp1 >= 0 and disp2 >= 0:
regPairs[i] += 1
return regPairsNow to do the optimization, I wrote a method that would handle the decision-making and utilize all the methods that were written beforehand.
Optimization Code
def optimize(current_region, further): # Determine next dispense
global quantities, regIdx
# Determine Region Pairs
regPair = regionPairs(quantities)
maxPair = regPair.index(max(regPair))
# Optimize based on Current Position and what needs to be dispensed
# First Determine Next Region
if current_region == 0 and further == False: # Starting Case
# End Goal is to either go to Region 1 or 4
cmp25 = regPair[2-1] > regPair[5-1]
cmp36 = regPair[3-1] > regPair[6-1]
if cmp25 == True and cmp36 == True: # Region 23 beats 36 on both
nxtRegion = 1
elif (cmp25 == True and cmp36 == False) or (cmp25 == False and cmp36 == True): # 23 does not dominate
cmp = [0, regPair[2-1], regPair[3-1], 0, regPair[5-1], regPair[6-1]]
if cmp.index(max(cmp)) in [1, 2]: # Largest Pair is in 2/3
nxtRegion = 1
else: # Largest Pair is in 5/6
nxtRegion = 4
elif (regPair[2-1] == regPair[5-1]) == True and (regPair[3-1] == regPair[6-1]) == True: # All the same number
if regPair[1-1] > regPair[4-1]:
nxtRegion = 1
else:
nxtRegion = 4
else:
nxtRegion = 4
elif current_region in [1, 4]:
if checkEmpty([quantities[regIdx[current_region-1][0]], quantities[regIdx[current_region-1][1]]]) == True: # Clear out any singles
nxtRegion = current_region
elif checkEmpty([quantities[regIdx[current_region][0]], quantities[regIdx[current_region][1]]]) == False: # 2/5 has something
nxtRegion = current_region + 1
elif checkEmpty([quantities[regIdx[current_region+1][0]], quantities[regIdx[current_region+1][1]]]) == False: #3/6 has something
nxtRegion = current_region + 2
else: # Means the rest is empty, next is to go back to home
nxtRegion = 0
elif current_region in [2, 5]:
if checkEmpty([quantities[regIdx[current_region-1][0]], quantities[regIdx[current_region-1][1]]]) == True: # Clear out any singles
nxtRegion = current_region
elif checkEmpty([quantities[regIdx[current_region][0]], quantities[regIdx[current_region][1]]]) == False: # 3/6 has something
nxtRegion = current_region + 1
else: # Next Region is empty so skip it
if current_region == 2:
nxtRegion = current_region + 2
else:
nxtRegion = 1
elif current_region in [3, 6]:
if checkEmpty([quantities[regIdx[current_region-1][0]], quantities[regIdx[current_region-1][1]]]) == True: # Clear out any singles
nxtRegion = current_region
elif current_region == 3:
if checkEmpty([quantities[regIdx[current_region][0]], quantities[regIdx[current_region][1]]]) == False: # 4 has something
nxtRegion = current_region + 1
elif current_region == 6:
if checkEmpty([quantities[regIdx[0][0]], quantities[regIdx[0][1]]]) == False: # 1 has something
nxtRegion = 1
else: # Next Region is empty so skip it, go home
nxtRegion = 0
# Next Determine if the Dispense is a Dual
if regPair[nxtRegion - 1] > 0: # If Region Number has a Dual
nxtDoub = True
nxtFurth = True # If Dual, cup will end up in further position
else: # If Region Number is only a single
nxtDoub = False
# Determine if single dispense is in further position
if quantities[regIdx[nxtRegion-1][0]] > 0:
nxtFurth = True
else: # Close Position
nxtFurth = False
return nxtRegion, nxtDoub, nxtFurth
Last updated