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:

IMG_8672.jpg


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()
Previous
Previous

Wearable LED + Acrylic Art

Next
Next

“Inside!” Button for Dog