Robotic Gumball Machine
My neighbor was cleaning out his kid's old stuff and gifted me two gumball machines. I replaced the cracked glass top with a laser cut acrylic cube and mechanized the dispenser. There is a Python program running on the Raspberry Pi that allows you choose a trivia category (originally Science, Adult Themed, Burning Man History, or Random). If you get 3 questions right out of 5 tries then it dispenses a gumball. I've since modified the program so the categories are general science, math, physics, and local history. It now lives in my classroom. The program contains a list of my current students and it only allow them one attempt every 24 hours.
Motivation
I use a Raspberry Pi based system in the high school classes I teach to manage students entering tardy or needing to leave the room during class time. As part of that system I code in a “disco mode",” where students could try to answer some random trivia questions to make the computer flash its lights and play The Hustle. I saw an opportunity to engage students. If I could make the reward enticing enough (gumballs), I could trick kids into learning.
I had received two gumball machines from my neighbor when his kids left for college and had been looking for something to do with them.
Process
After the initial tear down, I found that I could mechanize the dispenser part of the gumball machine if I was able to get the main gear of the turning knob fit on the shaft of a motor. I had a strong, 12V gear motor on hand that I’d purchased from an online surplus store. With some light machining I was able to fit the cog onto the shaft of the motor and key it into place. Four small holes drilled through the front face of the gumball machine allowed me to fix the motor in place. It would have been nice to keep the motor internal, but this was the easiest solution that presented itself with the materials on hand.
Next I set about testing some code with the general purpose input/output pins (GPIOs) of a Raspberry Pi. I built a simple test circuit with a 3.3V relay and a Python program to see if everything was working as expected:
I initially thought I might mount the Raspberry Pi, screen, and buttons on top of the glass enclosure, but after running the glass through my dishwasher it got a large crack in it, leading me to replace the whole upper half with acrylic. I used this opportunity to house the electronics in the first half of the enclosure, leaving room in the back for gumballs.
Screen
The screen I’m using is one of my favorites and has been incorperated in to many of my Raspberry Pi projects. I buy them on Amazon (“ELECROW 5 Inch Touch Screen 800x480 TFT LCD Display HDMI Interface” or “kuman 5 inch Resistive Touch Screen”) for about $40.
Setting up the screen takes a little work
Open terminal and update the repositories:
1. sudo apt-get update
An upgrade to the whole system isn't needed but it is recommended:
2. sudo apt-get upgrade
Now for the configuration:
3. Open terminal and use the command: sudo nano /boot/config.txt
4. Copy and paste this into the /boot/config.txt
___________________________________________________
hdmi_group=2
hdmi_mode=1
hdmi_mode=87
hdmi_cvt 800 480 60 6 0 0 0
dtparam=spi=ondtparam=i2c_arm=on
dtoverlay=ads7846,cs=1,penirq=25,penirq_pull=2,speed=50000,keep_vref_on=0,swapxy=0,pmax=255,xohms=150,xmin=200,xmax=3900,ymin=200,ymax=3900
dtoverlay=w1-gpio-pullup,gpiopin=4,extpullup=1
___________________________________________________
5. Ctrl + x and confirm file name(don't change name) press enter, type y and press enter to save changes.
6. Reboot
I also needed to flip my screen upside down
Add:
lcd_rotate=2
To the bottom of:
sudo nano /boot/config.txt
Lastly, to make the text bold and large enough to read, I messed around with some of the options here until it looked good:
sudo dpkg-reconfigure console-setup
Run program at boot
I also wanted the Raspberry Pi to run the program automatically every time it powered up.
Open the rc.local file with a text editor:
sudo nano /etc/rc.local
Then add commands below the comment, but leave the line exit() at the end. I added something like:
sudo python /home/pi/gumball.py
I think I ran it as root so it would be able to write to the .csv files.
Code
#Python program to run on Raspberry Pi for triva gumball machine #Johnny Devine 2018 #Program contains some ASCI art #Import Libraries import RPi.GPIO as GPIO import time, os, csv, random #Prepare the pins on the Raspberry Pi GPIO.setmode(GPIO.BOARD) GPIO.setwarnings(False) GPIO.cleanup() #Declare and define variables buttonA = 36 buttonB = 37 buttonC = 38 buttonD = 40 relayPin = 33 question_count = 0 question_count = 0 correct_count = 0 missed_count = 0 current_question = 0 asked_questions = [] button_pressed = False accept_intput = False valid_question = False restart = True #Set up GPIOs button_list = [buttonA, buttonB, buttonC, buttonD] GPIO.setup(button_list, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(relayPin, GPIO.OUT) #Open CSV file with questions. I used Excel to create. #Structure of file is: GENRE;DIFFICULTY;QUESTION;ANSWER_A;ANSWER_B;ANSWER_C;ANSWER_D;CORRECT_ANSWER questions_file = "questions.csv" question_set = sum(1 for row in open(questions_file)) #Define interupts (what happens when a button is pressed) def pressedA(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "A" def pressedB(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "B" def pressedC(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "C" def pressedD(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "D" #Interupt conditions GPIO.add_event_detect(buttonA, GPIO.FALLING, callback=pressedA, bouncetime=500) GPIO.add_event_detect(buttonB, GPIO.FALLING, callback=pressedB, bouncetime=500) GPIO.add_event_detect(buttonC, GPIO.FALLING, callback=pressedC, bouncetime=500) GPIO.add_event_detect(buttonD, GPIO.FALLING, callback=pressedD, bouncetime=500) ########################### #Main body of code: try: while True: if restart == True: print("\033c") print("\033[1;31;40m ____ _ _ _ _ \n\033[1;33;40m / ___|_ _ _ __ ___ | |__ __ _| | |___ | |\n\033[1;32;40m | | _| | | | '_ ` _ \| '_ \ / _` | | / __| | |\n\033[1;34;40m | |_| | |_| | | | | | | |_) | (_| | | \__ \ |_|\n\033[1;35;40m \____|\__,_|_| |_| |_|_.__/ \__,_|_|_|___/ (_)") print("\n\033[1;37;40m Get three questions correct to win.\n\n You can miss two questions.\n\n Press ANY BUTTON to continue...") asked_questions = [] accept_input = True question_count = 0 correct_count = 0 missed_count = 0 button_pressed = False while button_pressed == False: time.sleep(0.1) button_pressed = False accept_input = False restart = False elif question_count > 5 or missed_count > 2: print("\033c") print(" __ __ \n \ \ / / \n \ \_/ /__ _ _ \n \ / _ \| | | | \n _ | | (_) | |_| | _ \n | | |_|\___/ \__,_|| |\n | | ___ ___ ___ | |\n | |/ _ \/ __|/ _ \ | |\n | | (_) \__ \ __/ |_|\n |_|\___/|___/\___| (_)") print("\n\n Sorry, you did not win.") restart = True time.sleep(3) elif correct_count >= 3: print("\033c") print(" __ ___ _ \n \ \ / (_)_ __ _ __ ___ _ __ | |\n \ \ /\ / /| | '_ \| '_ \ / _ \ '__| | |\n \ V V / | | | | | | | | __/ | |_|\n \_/\_/ |_|_| |_|_| |_|\___|_| (_)\n ") print("\n\n You win! Have a gumball!") restart = True GPIO.output(relayPin, GPIO.HIGH) time.sleep(3) GPIO.output(relayPin, GPIO.LOW) else: print("\033c") valid_question = False while valid_question == False: current_question = random.randint(1,question_set-1) if current_question not in asked_questions: valid_question = True asked_questions.append(current_question) question_count += 1 print("This is question ", question_count) print("You need to get",3-correct_count,"more questions right.") print("You can get",2-missed_count,"more questions wrong.\n") with open("questions.csv") as csvfile: readCSV = csv.reader(csvfile,delimiter=';') questions = [] choiceAs = [] choiceBs = [] choiceCs = [] choiceDs = [] rightAnswers = [] for row in readCSV: question = row[2] choiceA = row[3] choiceB = row[4] choiceC = row[5] choiceD = row[6] rightAnswer = row[7] questions.append(question) choiceAs.append(choiceA) choiceBs.append(choiceB) choiceCs.append(choiceC) choiceDs.append(choiceD) rightAnswers.append(rightAnswer) print(questions[current_question]) print("A: ",choiceAs[current_question]) print("B: ",choiceBs[current_question]) print("C: ",choiceCs[current_question]) print("D: ",choiceDs[current_question]) accept_input = True while button_pressed == False: time.sleep(0.1) button_pressed = False accept_input = False if answer_choice == rightAnswers[current_question]: print("\033[1;32;40m ______ __ __\n / ____/___ _____________ _____/ /_ / /\n / / / __ \/ ___/ ___/ _ \/ ___/ __/ / / \n/ /___/ /_/ / / / / / __/ /__/ /_ /_/ \n\____/\____/_/ /_/ \___/\___/\__/ (_) \n ") correct_count += 1 time.sleep(1) else: print("\033[1;31;40m _ __ __\n| | / /________ ____ ____ _ / /\n| | /| / / ___/ __ \/ __ \/ __ `/ / / \n| |/ |/ / / / /_/ / / / / /_/ / /_/ \n|__/|__/_/ \____/_/ /_/\__, / (_) \n /____/ ") missed_count += 1 time.sleep(1) except: print("exception raised") KeyboardInterrupt GPIO.cleanup()
Modified Code
This code includes a check to see if students are in fact part of my class and keeps track of the last time the attempted to win a gumball (only one attempt every 24 hours). I update the list of students each semester with a .csv file that I can easily export from my grade book. I also added a 10-key pad to the gumball machine so students could enter their student ID number (which is what keeps track of their attempts and is what the code uses for identifying the students in my class list).
#Python program to run gumball triva game, with checks on student identiy and restrictions on frequency of use. #For additional comments about code, see the original code. #Johnny Devine 2018 import RPi.GPIO as GPIO import time, os, csv, random, textwrap, datetime GPIO.setmode(GPIO.BOARD) GPIO.setwarnings(False) GPIO.cleanup() #This special student ID number gets unlimited attempts: master_id = "1122334" #This code is needed to start the program, so if I unplug the machine #and don't want students using it, they can't get past the welcome screen start_password = "1122334" buttonA = 36 buttonB = 37 buttonC = 38 buttonD = 40 shutdown_button = 35 relayPin = 33 question_count = 0 question_count = 0 correct_count = 0 missed_count = 0 current_question = 0 asked_questions = [] button_pressed = False accept_intput = False valid_question = False restart = True newTry = False pass_to_main_menu = False time_to_answer = 20 #time in seconds to answer each question. Added to avoid students Googling answers #Variables needed to handle student information in .csv files path_to_student_list = "/home/pi/studentlist.csv" path_to_studentevents = "/home/pi/studentevents.csv" student_row = 0 student_name = "" student_id = "" student_grade = "" student_gender = "" student_birthday = "" button_list = [buttonA, buttonB, buttonC, buttonD, shutdown_button] GPIO.setup(button_list, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(relayPin, GPIO.OUT) def pressedA(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "A" def pressedB(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "B" def pressedC(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "C" def pressedD(channel): global button_pressed, answer_choice, accept_input if accept_input == True: button_pressed = True answer_choice = "D" def shutdown(channel): os.system("sudo shutdown -h now") GPIO.add_event_detect(buttonA, GPIO.FALLING, callback=pressedA, bouncetime=500) GPIO.add_event_detect(buttonB, GPIO.FALLING, callback=pressedB, bouncetime=500) GPIO.add_event_detect(buttonC, GPIO.FALLING, callback=pressedC, bouncetime=500) GPIO.add_event_detect(buttonD, GPIO.FALLING, callback=pressedD, bouncetime=500) GPIO.add_event_detect(shutdown_button, GPIO.FALLING, callback = shutdown, bouncetime=500) #This chunk of code uses the student's ID number to identify them def IdentifyStudent(id): global pass_to_main_menu, student_row, student_name, student_id, student_grade, student_gender, student_birthday with open(path_to_student_list) as csvfile: readcsv = csv.reader(csvfile,delimiter=',') names = [] id_numbers = [] grades = [] genders = [] birthdays = [] for row in readcsv: name = row[0] id_number = row[1] grade = row[2] gender = row[3] birthday = row[4] names.append(name) id_numbers.append(id_number) grades.append(grade) birthdays.append(birthday) escape = False pass_to_main_menu = False while escape == False: if id == "0": escape = True pass_to_main_menu = True elif id == "8675309": os.system("sudo shutdown -h now") elif id not in id_numbers or id == "" and id != master_id: print("\033c") #print("ID was not found in Devine's class lists.") #id = input("\nTry again, or enter '0' to return to main menu\n: ") print("\033[1;31;40m ____ _ _ _ _ \n\033[1;33;40m / ___|_ _ _ __ ___ | |__ __ _| | |___ | |\n\033[1;32;40m | | _| | | | '_ ` _ \| '_ \ / _` | | / __| | |\n\033[1;34;40m | |_| | |_| | | | | | | |_) | (_| | | \__ \ |_|\n\033[1;35;40m \____|\__,_|_| |_| |_|_.__/ \__,_|_|_|___/ (_)") print("\nAnswer trivia and get a gumball!\n\n\033[1;31;40mSorry, that ID was not found in Devine's class lists.") time.sleep(2) escape = True else: CheckIfNewTry(id) escape = True reference_index = id_numbers.index(id) student_row = reference_index student_name = names[reference_index] student_id = id_numbers[reference_index] student_grade = grades[reference_index] student_gender = grades[reference_index] student_birthday = birthdays[reference_index] #This adds the student's use of the machine to a .csv file def AddStudentEvent(): global student_name, student_id data_to_write = [student_name, student_id, datetime.datetime.now()] with open(path_to_studentevents, 'a') as eventfile: eventwriter = csv.writer(eventfile) eventwriter.writerow(data_to_write) for i in range(3, len(data_to_write)): data_to_write[i] = 0 #This checks the .csv of events to see if the student ID was used in the last 24 hours def CheckIfNewTry(id): global newTry with open(path_to_studentevents) as eventfile: eventReader = csv.reader(eventfile,delimiter=',') names = [] id_numbers = [] timestamps = [] for row in eventReader: name = row[0] id_number = row[1] timestamp = row[2] names.append(name) id_numbers.append(id_number) timestamps.append(timestamp) newTry = True now = datetime.datetime.now() counter = 0 for entry in id_numbers: then = datetime.datetime.strptime(timestamps[counter], "%Y-%m-%d %H:%M:%S.%f") if str(id_numbers[counter]) == str(id): delta_time = now - then if delta_time.total_seconds() < 86400: newTry = False counter += 1 if newTry == False and str(id) != master_id: print("\033[1;31;40m\n\nSorry, only one attempt can be made every 24 hours.") time.sleep(3) else: newTry = True try: start_lock = True while start_lock = True: print("\033c") unlock_code = input("Password: ") if unlock_code == start_password: start_lock = False else: print("Tsk, tsk, you don't know the password...") time.sleep(2) while True: if restart == True and newTry == False: print("\033c") print("\033[1;31;40m ____ _ _ _ _ \n\033[1;33;40m / ___|_ _ _ __ ___ | |__ __ _| | |___ | |\n\033[1;32;40m | | _| | | | '_ ` _ \| '_ \ / _` | | / __| | |\n\033[1;34;40m | |_| | |_| | | | | | | |_) | (_| | | \__ \ |_|\n\033[1;35;40m \____|\__,_|_| |_| |_|_.__/ \__,_|_|_|___/ (_)") id_number_input = input("\nAnswer trivia and get a gumball!\n\nPlease enter student ID number: ") IdentifyStudent(id_number_input) elif restart == True and newTry == True: print("\033c") print("\033[1;31;40m ____ _ _ _ _ \n\033[1;33;40m / ___|_ _ _ __ ___ | |__ __ _| | |___ | |\n\033[1;32;40m | | _| | | | '_ ` _ \| '_ \ / _` | | / __| | |\n\033[1;34;40m | |_| | |_| | | | | | | |_) | (_| | | \__ \ |_|\n\033[1;35;40m \____|\__,_|_| |_| |_|_.__/ \__,_|_|_|___/ (_)") print("\n\033[1;37;40m Get three questions correct to win.\n\n You can miss two questions.\n\n Select your category:\n\n \033[1;31;40mA: MATH \033[1;33;40mB: GENERAL SCIENCE \033[1;37;40mC: PHYSICS \033[1;34;40mD: TACOMA") asked_questions = [] accept_input = True question_count = 0 correct_count = 0 missed_count = 0 button_pressed = False while button_pressed == False: time.sleep(0.1) button_pressed = False accept_input = False restart = False if answer_choice == "A": questions_file = "mathquestions.csv" question_set = sum(1 for row in open(questions_file)) elif answer_choice =="B": questions_file = "sciencequestions.txt" question_set = sum(1 for row in open(questions_file)) elif answer_choice == "C": questions_file = "physicsquestions.csv" question_set = sum(1 for row in open(questions_file)) else: questions_file = "tacomaquestions.csv" question_set = sum(1 for row in open(questions_file)) elif question_count > 5 or missed_count > 2: print("\033c") print("\033[1;37;40m") print(" __ __ \n \ \ / / \n \ \_/ /__ _ _ \n \ / _ \| | | | \n _ | | (_) | |_| | _ \n | | |_|\___/ \__,_|| |\n | | ___ ___ ___ | |\n | |/ _ \/ __|/ _ \ | |\n | | (_) \__ \ __/ |_|\n |_|\___/|___/\___| (_)") print("\n\n Sorry, you did not win.") restart = True time.sleep(3) newTry = False AddStudentEvent() elif correct_count >= 3: print("\033c") print("\033[1;37;40m") print(" __ ___ _ \n \ \ / (_)_ __ _ __ ___ _ __ | |\n \ \ /\ / /| | '_ \| '_ \ / _ \ '__| | |\n \ V V / | | | | | | | | __/ | |_|\n \_/\_/ |_|_| |_|_| |_|\___|_| (_)\n ") print("\n\n You win! Have a gumball!") restart = True GPIO.output(relayPin, GPIO.HIGH) time.sleep(3.5) GPIO.output(relayPin, GPIO.LOW) newTry = False AddStudentEvent() else: print("\033c") print("\033[1;37;40m") valid_question = False while valid_question == False: current_question = random.randint(1,question_set-1) if current_question not in asked_questions: valid_question = True asked_questions.append(current_question) question_count += 1 print("This is question ", question_count) print("You need to get",3-correct_count,"more questions right.") print("You can get",2-missed_count,"more questions wrong.\n") with open(questions_file) as csvfile: readCSV = csv.reader(csvfile,delimiter=';') questions = [] choiceAs = [] choiceBs = [] choiceCs = [] choiceDs = [] rightAnswers = [] for row in readCSV: question = row[2] choiceA = row[3] choiceB = row[4] choiceC = row[5] choiceD = row[6] rightAnswer = row[7] questions.append(question) choiceAs.append(choiceA) choiceBs.append(choiceB) choiceCs.append(choiceC) choiceDs.append(choiceD) rightAnswers.append(rightAnswer) wrapped_question = textwrap.wrap(questions[current_question],50) for line in wrapped_question: print(line) print("A: ",choiceAs[current_question]) print("B: ",choiceBs[current_question]) print("C: ",choiceCs[current_question]) print("D: ",choiceDs[current_question]) accept_input = True #begin coroutine somewhere around here that count down from 10 seconds #if time exceeded, then answer_choice = not the right answer! start_time = time.time() time_elapsed = 0 timed_out = False while button_pressed == False and time_elapsed <= time_to_answer: time.sleep(0.1) time_elapsed = time.time() - start_time if time_elapsed >= time_to_answer: timed_out = True button_pressed = False accept_input = False if answer_choice == rightAnswers[current_question]: print("\033[1;32;40m ______ __ __\n / ____/___ _____________ _____/ /_ / /\n / / / __ \/ ___/ ___/ _ \/ ___/ __/ / / \n/ /___/ /_/ / / / / / __/ /__/ /_ /_/ \n\____/\____/_/ /_/ \___/\___/\__/ (_) \n ") correct_count += 1 time.sleep(1) elif timed_out == True: print("\033c") print("\033[1;31;40m _______ _____ __ __ ______ _____ _ _ _____ \n |__ __|_ _| \/ | ____|/ ____| | | | | __ \ \n | | | | | \ / | |__ | (___ | | | | |__) |\n | | | | | |\/| | __| \___ \ | | | | ___/ \n | | _| |_| | | | |____ ____) | | |__| | | \n |_| |_____|_| |_|______|_____/ \____/|_| ") missed_count += 1 time.sleep(2) else: print("\033[1;31;40m _ __ __\n| | / /________ ____ ____ _ / /\n| | /| / / ___/ __ \/ __ \/ __ `/ / / \n| |/ |/ / / / /_/ / / / / /_/ / /_/ \n|__/|__/_/ \____/_/ /_/\__, / (_) \n /____/ ") missed_count += 1 time.sleep(1) except: print("exception raised") KeyboardInterrupt GPIO.cleanup()