/**************************************************************************************************
* Project Author: Israel Charles
* Project Name: Vending Machine Simulation
*
* Project Folder: Project_VendingMachine
* Main File: VendingMachineSim.ino
* Version: 1.0
* Last Modification: 04/21/2024
*
* Project Description: Using a ESP32Wroom Development board, and the FreeRTOS as an Real-Time
* Operating System, this project implements the firmware of a vending machine
**************************************************************************************************/
//********************************* Program Libraries *******************************************//
#include <Arduino.h>
//************************** Pins and Messages Definition ***************************************//
#define BLUE_LED 2 // LED will indicate how the program is running
#define END_INPUT '\n' // Character that indicate the end of user input
#define INVALID_INPUT "X" // String that indicate an invalid input
// Input to enter when choosing which soda to dispense
#define COLA_ID '1'
#define GRAPE_ID '2'
#define ORANGE_ID '3'
#define LIME_ID '4'
// Output message when user input invalid character(s)
#define INVALID_MESSAGE "*** Your input contains invalid character(s). ***\n"\
"*** Please follow the given instruction. ***"
// Welcome meesage that output during the set up function
#define WELCOME " *** Welcome to Izi Vending Machine ***\n\n"\
" -- Let's satisfy your thirst!! --\n ---\n"
// Instruction to insert coins in the machine
#define INSTRUCTION "____________________________________________________\n"\
"----------------------------------------------------\n"\
"INSTRUCTIONS\nTo enter coins, type the respective letter according" \
"\nto the following list:\n"\
"\n - Q -> $0.25 (Quarter)\n - D -> $0.10 (Dimes)\n - N -> $0.05 (Nickels)\n\n"\
"--> Then press 'Enter' to finalise your input\n\n** Note **\n"\
" - Stack inputs are accepted\n\ - Inputs are Case Incentives\n"\
"(Ex: Type 'QdN' then press 'Enter' to insert $0.40)\n\n"\
"After inserting the coin(s), type the ID of the Soda\nto dispense,"\
" then press 'Enter'\nOr type 'C' to cancel purchase then press Enter.\n"\
"Change is dispensed after each purchase, if\nnecessary.\n"\
"____________________________________________________\n"\
"----------------------------------------------------\n"
// Tabular output that has the soda menu forma
#define MENU_TITLE " --------------------------------------------------"\
"\n|ID Soda Price($) Available|"\
"\n|--------------------------------------------------|"
// To output information about cola soda in the right format
#define COLA_INFO "|%c Cola $%.2f %2d |"
// To output information about grape soda in the right format
#define GRAPE_INFO "|%c Grape $%.2f %2d |"
// To output information about orange soda in the right format
#define ORANGE_INFO "|%c Orange $%.2f %2d |"
// To output information about lime soda in the right format
#define LIME_INFO "|%c Lime $%.2f %2d |\n"\
" --------------------------------------------------"
// Message that outputs the amount of each coin returned after each purchase
#define COINS_RETURNED "Quarter(s) returned => %d\nDime(s) returned => %d\n"\
"Nickel(s) returned => %d"
// Outputing the amount of coins available in the machine
#define COINS_AVAILABLE " --------------- \n"\
"|Coins Available|\n"\
" --------------- \n"\
"|Quarter(s) %2d |\n"\
"|Dime(s) %2d |\n"\
"|Nickel(s) %2d |\n"\
" --------------- \n"
// Message for random error 1
#define ERROR_1_MESSAGE "****************************************************\n"\
"*** ERROR CODE: 1 ***\n"\
"*** ERROR DESCRIPTION: VENDING MACHINE HAS BEEN ***\n"\
"*** REMOTELY DISABLED. ***\n"\
"*** ERROR RESOLUTION: TYPE THE REACTIVATION CODE***\n"\
"*** AND PRESS 'ENTER'. ***\n"\
"****************************************************\n"\
"* Reactivation code: 'IziUnlock' (Case Sensitive) *\n"\
"****************************************************\n"\
"\nENTER CODE ==> "
// Unlock code that fixes error 1
#define UNLOCK_CODE "IziUnlock"
// Message for random error 2
#define ERROR_2_MESSAGE "****************************************************\n"\
"*** ERROR CODE: 2 ***\n"\
"*** ERROR DESCRIPTION: MAINTNANCE REQUIRED - COIN***\n"\
"*** COUNTER JAMMED ***\n"\
"*** ERROR RESOLUTION: UNCLOG COUNTER AND PRESS ***\n"\
"*** 'RESET' BUTTON ON MACHINE ***\n"\
"****************************************************\n"
// Message for random error 3
#define ERROR_3_MESSAGE "****************************************************\n"\
"*** ERROR CODE: 3 ***\n"\
"*** ERROR DESCRIPTION: POWER FLUCTUATION ***\n"\
"*** ERROR RESOLUTION: MACHINE EXPERIENCED MAJOR ***\n"\
"*** POWER FLUCTUATION. PLEASE ***\n"\
"*** RESET MACHINE ***\n"\
"****************************************************\n"
// Message for random error 4
#define ERROR_4_MESSAGE "****************************************************\n"\
"*** ERROR CODE: 4 ***\n"\
"*** ERROR DESCRIPTION: MAINTNANCE REQUIRED - SODA***\n"\
"*** DISPENSER CABLE BROKEN ***\n"\
"*** ERROR RESOLUTION: REPLACE CABLE AND PRESS ***\n"\
"*** 'RESET' BUTTON ON MACHINE ***\n"\
"****************************************************\n"
//************************* Global Variables and constants **************************************//
// Number of Coins
int numQuarters = 8; // Initial number of Quarters in the machine
int numDimes = 20; // Initial number of Dimes in the machine
int numNickels = 40; // initial number of Nickels in the machine
// Number of Sodas
int numColaSoda = 10; // Initial number of Cola Soda in the machine
int numOrangeSoda = 10; // Initial number of Orange Soda in the machine
int numGrapeSoda = 10; // Initial number of Grape Soda in the machine
int numLimeSoda = 10; // Initial number of Lime Soda in the machine
// Price of Sodas in cents to prevent rounding error if working with double
const int COLA_SODA_PRICE = 25; // Price of a single Cola Soda in cents
const int GRAPE_SODA_PRICE = 30; // Price of a single Grape Soda in cents
const int ORANGE_SODA_PRICE = 45; // Price of a single Orange Soda in cents
const int LIME_SODA_PRICE = 40; // Price of a single Lime Soda in cents
// Input characters definition
const char INPUT_NICKEL = 'N'; // Input that represents a nickel or $0.05
const char INPUT_DIME = 'D'; // Input that represents a dime or $0.10
const char INPUT_QUARTER = 'Q'; // Input that represents a Quarter or $0.25
const char CANCEL_OPTION = 'C'; // Input that cancels an ongoing purchase
// Input/Output related variables
String inputString; // Variable that will hold of the input
char inputChar; // Variable that transmit each character of input
char outputString[200]; // Variable that store characters to output
int inputCoinTotal = 0; // Stores the amount of money inserted in cents
int insertedQuarter = 0; // Store the number of Quarters inserted
int insertedDime = 0; // Store the number of Dimes inserted
int insertedNickel = 0; // Store the number of Nickels inserted
//************************** Handles and Function prototypes ************************************//
// Handles Definition
TaskHandle_t User_Input;
TaskHandle_t Display_Menu;
TaskHandle_t Random_Errors;
// Function Prototypes for Tasks
void Task_Random_Errors(void * pvParameters);
void Task_Display_Menu(void * pvParameters);
void Task_User_Input(void * pvParameters);
// Helper Function Prototypes
String getSodaInput(int numChoices, char choiceArray[]);
String getCoinInput(int numChoices, char choiceArray[]);
bool isCharIn (char charToCheck, int arrayLength, char arrayOption[]);
char toUpper(char charToConvert);
void updateSodaStatus(char charIn);
void updateInsertStatus(char charIn);
void cancelPurchase();
bool processChange(double moneyToGive);
double priceInDollar(int cents);
//**************************** Initialization/Setup Function ************************************//
// Function that create the different tasks and set up Serial Communication
void setup() {
// Setting the Pin for the Blue LED as output
pinMode(BLUE_LED, OUTPUT);
// Setting communication rate as 9600 Baud Rate
Serial.begin(9600);
// Seting the seed for the random generator function
pinMode(0, INPUT);
randomSeed(analogRead(0));
// Outputing the welcome message and instructions on the terminal
Serial.println();
Serial.println(INSTRUCTION);
Serial.println(WELCOME);
// Defining a data type that will receive the return value of when creating a task
BaseType_t TaskCreationReturn;
// Create a Random Error generator Task and return the status of the task (Created or Not)
TaskCreationReturn = xTaskCreatePinnedToCore(&Task_Random_Errors,
"Random Error Task",
2000,
NULL,
3,
&Random_Errors,
1);
// Checking if task was created successfully. If not, indicate error with Blue LED
// BLUE LED blinking 3 times means Display Menu Task was not created successfully
if (TaskCreationReturn != pdPASS){
for (int i = 0; i < 3; i++){
digitalWrite(BLUE_LED, HIGH); // Turn LED ON
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
digitalWrite(BLUE_LED, LOW); // Turn LED OFF
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
}
}
// Create a Display Menu Task and return the status of the task (Created or Not)
TaskCreationReturn = xTaskCreatePinnedToCore(&Task_Display_Menu,
"Display Menu Task",
10000,
NULL,
2,
&Display_Menu,
0);
// Checking if task was created successfully. If not, indicate error with Blue LED
// BLUE LED blinking 4 times means Display Menu Task was not created successfully
if (TaskCreationReturn != pdPASS){
for (int i = 0; i < 4; i++){
digitalWrite(BLUE_LED, HIGH); // Turn LED ON
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
digitalWrite(BLUE_LED, LOW); // Turn LED OFF
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
}
}
// Create User Taking Input Task and return the status of the task (Created or Not)
TaskCreationReturn = xTaskCreatePinnedToCore(&Task_User_Input,
"User Input Task",
10000,
NULL,
1,
&User_Input,
0);
// Checking if task was created successfully. If not, indicate error with Blue LED
// BLUE LED blinking 5 times means Taking Input Task was not created successfully
if (TaskCreationReturn != pdPASS){
for (int i = 0; i < 5; i++){
digitalWrite(BLUE_LED, HIGH); // Turn LED ON
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
digitalWrite(BLUE_LED, LOW); // Turn LED OFF
vTaskDelay(250 / portTICK_RATE_MS); // Wait 250 millisecond
}
}
// Turning the Blue LED ON to indicate that the program is running correctly
digitalWrite(BLUE_LED, HIGH);
// Delete the setup task since it's not needed
vTaskDelete(NULL);
}
//****************************** Random Error Generator Function ********************************//
// Function that randomly generate an error for the program
void Task_Random_Errors(void * pvParameters){
for(;;){
vTaskDelay(1000 / portTICK_RATE_MS); // Wait 1000 millisecond
}
}
//*********************************** Display Menu Task Function ********************************//
// Function that displays the Sodas for sale, their quatity, and their price
void Task_Display_Menu(void * pvParameters){
// Loop to stop the task from returning
for(;;){
// Displaying the table like menu title
Serial.println(MENU_TITLE);
// Displaying info about Cola soda
sprintf(outputString, COLA_INFO, COLA_ID, priceInDollar(COLA_SODA_PRICE), numColaSoda);
Serial.println(outputString);
// Displaying info about Grape soda
sprintf(outputString, GRAPE_INFO, GRAPE_ID, priceInDollar(GRAPE_SODA_PRICE), numGrapeSoda);
Serial.println(outputString);
// Displaying info about Orange soda
sprintf(outputString, ORANGE_INFO, ORANGE_ID, priceInDollar(ORANGE_SODA_PRICE), numOrangeSoda);
Serial.println(outputString);
// Displaying info about Lime soda
sprintf(outputString, LIME_INFO, LIME_ID, priceInDollar(LIME_SODA_PRICE), numLimeSoda);
Serial.println(outputString);
Serial.println();
// Displaying info abou the amount of coins available in the machine
sprintf(outputString, COINS_AVAILABLE, numQuarters, numDimes, numNickels);
Serial.println(outputString);
Serial.println();
// Suspending the display task so the program can take inputs
vTaskSuspend(NULL);
}
}
//*********************************** User Input Task Function **********************************//
// Function that takes in user coins input and user soda selection and dispense soda
void Task_User_Input(void * pvParameters){
// Infinite Loop for task to not return
for (;;){
inputString = ""; // Reseting the input string after each sale
inputCoinTotal = 0; // Reseting money inserted after each operation
insertedNickel = 0;
insertedDime = 0;
insertedQuarter = 0;
Serial.print("Please Insert Coins ==> ");
// Setting the selection of acceptable input to take from user
char acceptableCoins[] = {INPUT_NICKEL, INPUT_DIME, INPUT_QUARTER};
// Get user input through helper function given number of acceptable options and option array
inputString = getCoinInput(3, acceptableCoins);
// Checking if user had input something invalid
if (inputString == INVALID_INPUT){
Serial.println(INVALID_MESSAGE);
}
// Proceeding with selecting soda to purchase if input is valid
else{
inputString = ""; // Reseting the input string to get soda
Serial.print("Total Coin(s) Inserted => $");
Serial.println(priceInDollar(inputCoinTotal));
Serial.println("----");
Serial.print("Please select a soda ==> ");
// Setting the selection of acceptable input to take from user for the sodas
char acceptableInput[] = {COLA_ID, GRAPE_ID, ORANGE_ID, LIME_ID, CANCEL_OPTION};
// Get user input through helper function given number of acceptable options and option array
inputString = getSodaInput(5, acceptableInput);
// Checking if user had input something invalid
if (inputString == INVALID_INPUT){
Serial.println(INVALID_MESSAGE);
}
else{
updateSodaStatus(inputString.charAt(0));
}
}
Serial.println("----------------------------------------------------");
Serial.println("____________________________________________________\n");
// Resuming the Display menu task and execute since it has higher priority than current task
vTaskResume(Display_Menu);
vTaskDelay(250 / portTICK_RATE_MS); // Delay to not flag the watchdog timer
}
}
//********************************* Helper Functions called by Tasks ****************************//
// Input Control and Parse User Input function via serial input for inserting Coins
String getCoinInput(int numChoices, char choiceArray[]){
bool isInputValid = true; // Flag that sets if there was an invalid input
// Loop that process each character entered through Serial Input
do{
// Checking if there is a character to process from Serial input
if (Serial.available() > 0) {
inputChar = Serial.read(); // Save the character in the char input variable
// Converting all alphabetic characters to Upper Case for uniformity
inputChar = toUpper(inputChar);
inputString += inputChar; // Add character to input String variable
// Checking if the character is the END OF LINE (newline character)
if (inputChar != END_INPUT) {
// Processing non-space characters
if (inputChar != ' '){
// Checking if a character is a valid character. Flag it if it's not
if (!isCharIn(inputChar, numChoices, choiceArray)){
isInputValid = false;
}
else{
updateInsertStatus(inputChar); // Update coins inserted
}
}
}
// Finishing processing if character is a new line
else {
inputString.trim();
Serial.println(inputString);
// If all characters were valid return the input string
if (isInputValid){
return inputString;
}
// If a character was invalid, canceling all input and return invalid flag
else{
inputCoinTotal = 0;
return INVALID_INPUT;
}
}
}
vTaskDelay(250 / portTICK_RATE_MS); // Delay to not raise watch dog timer flag
} while (inputChar != END_INPUT); // looping until a character is a new line
}
// Input Control and Parse User Input function via serial input for inserting Soda
String getSodaInput(int numChoices, char choiceArray[]){
bool isInputValid = true; // Flag that sets if there was an invalid input
// Loop that process each character entered through Serial Input
do{
// Checking if there is a character to process from Serial input
if (Serial.available() > 0) {
inputChar = Serial.read(); // Save the character in the char input variable
// Converting all alphabetic characters to Upper Case for uniformity
inputChar = toUpper(inputChar);
inputString += inputChar; // Add character to input String variable
// Checking if the character is the END OF LINE (newline character)
if (inputChar == END_INPUT) {
// Process and output the received message
inputString.trim();
Serial.println(inputString);
// Checking if the input string is among the options
if (inputString.length() == 1 && isCharIn(inputString.charAt(0), numChoices, choiceArray)){
return inputString;
}
// If input is invalid return invalid flag
else{
return INVALID_INPUT;
}
}
}
vTaskDelay(250 / portTICK_RATE_MS); // Delay to not raise watch dog timer flag
} while (inputChar != END_INPUT); // looping until a character is a new line
}
// Function that checks if a character is in an array
bool isCharIn (char charToCheck, int arrayLength, char arrayOption[]){
// Looping through the array to see if the input is one of the option
for (int i = 0; i < arrayLength; i++){
if (charToCheck == arrayOption[i]){
return true;
}
}
return false;
}
// Function that convert a lower case character to upper case character
char toUpper(char charToConvert) {
// Converting only lowercase characters
if (charToConvert >= 'a' && charToConvert <= 'z') {
return charToConvert - ('a' - 'A');
}
return charToConvert;
}
// Functiont that counts money inserted and update coin counts
void updateInsertStatus (char charIn){
if (charIn == INPUT_NICKEL){
inputCoinTotal += 5;
insertedNickel++;
}
else if (charIn == INPUT_DIME){
inputCoinTotal += 10;
insertedDime++;
}
else if (charIn == INPUT_QUARTER){
inputCoinTotal += 25;
insertedQuarter++;
}
}
// Function that keep track of soda being sold
void updateSodaStatus(char charIn){
// Reseting money inserted and coins inserted coins if cancel option was chosen
if (charIn == CANCEL_OPTION){
Serial.println("Option selected ==> Cancel Purchase...");
Serial.println("Preocessing...\n----");
cancelPurchase();
}
// Selling that Soda if that option was chosen
else if (charIn == COLA_ID){
Serial.println("Option selected ==> Cola");
Serial.println("Processing...\n----");
//Checking if that soda is available
if (numColaSoda > 0){
// Checking if the money inserted is enough for the soda
if (inputCoinTotal >= COLA_SODA_PRICE){
// Checking if it's possible to give exact change back
if(processChange(inputCoinTotal - COLA_SODA_PRICE)){
numColaSoda--;
inputCoinTotal -= COLA_SODA_PRICE;
Serial.println("1 Cola Soda sold");
Serial.print("Total Change Returned => $");
Serial.println(priceInDollar(inputCoinTotal));
Serial.println();
Serial.println(outputString);
Serial.println("----");
}
else{
Serial.println("Exact Change is required for this purchase.");
cancelPurchase();
}
}
else{
Serial.println("Coin(s) inserted is insufficient for this purchase");
cancelPurchase();
}
}
else{
Serial.println("Machine is out of the selected item");
cancelPurchase();
}
}
// Selling that Soda if that option was chosen
else if (charIn == GRAPE_ID){
Serial.println("Option selected ==> Grape");
Serial.println("Processing...\n----");
//Checking if that soda is available
if (numGrapeSoda > 0){
// Checking if the money inserted is enough for the soda
if (inputCoinTotal >= GRAPE_SODA_PRICE){
// Checking if it's possible to give exact change back
if(processChange(inputCoinTotal - GRAPE_SODA_PRICE)){
numGrapeSoda--;
inputCoinTotal -= GRAPE_SODA_PRICE;
Serial.println("1 Grape Soda sold");
Serial.print("Total Change Returned => $");
Serial.println(priceInDollar(inputCoinTotal));
Serial.println();
Serial.println(outputString);
Serial.println("----");
}
else{
Serial.println("Exact Change is required for this purchase.");
cancelPurchase();
}
}
else{
Serial.println("Coin(s) inserted is insufficient for this purchase");
cancelPurchase();
}
}
else{
Serial.println("Machine is out of the selected item");
cancelPurchase();
}
}
// Selling that Soda if that option was chosen
else if (charIn == LIME_ID){
Serial.println("Option selected ==> Lime");
Serial.println("Processing...\n----");
//Checking if that soda is available
if (numLimeSoda > 0){
// Checking if the money inserted is enough for the soda
if (inputCoinTotal >= LIME_SODA_PRICE){
// Checking if it's possible to give exact change back
if(processChange(inputCoinTotal - LIME_SODA_PRICE)){
numLimeSoda--;
inputCoinTotal -= LIME_SODA_PRICE;
Serial.println("1 Lime Soda sold");
Serial.print("Total Change Returned => $");
Serial.println(priceInDollar(inputCoinTotal));
Serial.println();
Serial.println(outputString);
Serial.println("----");
}
else{
Serial.println("Exact Change is required for this purchase.");
cancelPurchase();
}
}
else{
Serial.println("Coin(s) inserted is insufficient for this purchase");
cancelPurchase();
}
}
else{
Serial.println("Machine is out of the selected item");
cancelPurchase();
}
}
// Selling that Soda if that option was chosen
else if (charIn == ORANGE_ID){
Serial.println("Option selected ==> Orange");
Serial.println("Processing...\n----");
//Checking if that soda is available
if (numOrangeSoda > 0){
// Checking if the money inserted is enough for the soda
if (inputCoinTotal >= ORANGE_SODA_PRICE){
// Checking if it's possible to give exact change back
if(processChange(inputCoinTotal - ORANGE_SODA_PRICE)){
numOrangeSoda--;
inputCoinTotal -= ORANGE_SODA_PRICE;
Serial.println("1 Orange Soda sold");
Serial.print("Total Change Returned => $");
Serial.println(priceInDollar(inputCoinTotal));
Serial.println();
Serial.println(outputString);
Serial.println("----");
}
else{
Serial.println("Exact Change is required for this purchase.");
cancelPurchase();
}
}
else{
Serial.println("Coin(s) inserted is insufficient for this purchase");
cancelPurchase();
}
}
else{
Serial.println("Machine is out of the selected item");
cancelPurchase();
}
}
}
// Function that cancels a purchase and reset money and coins inserted
void cancelPurchase(){
inputCoinTotal = 0;
insertedNickel = 0;
insertedDime = 0;
insertedQuarter = 0;
Serial.println("Purchase has been cancelled.\nCoin(s) have been returned.");
}
// Function that gives change back to user. Returns false if unable to
bool processChange(double moneyToGive){
double leftOver = moneyToGive; // Variable used to parse the change to give
// Store the amount of each coins to give based on calculation and availability
int quarterToGive = 0;
int dimeToGive = 0;
int nickelToGive = 0;
// Checking how many quarters to give if change is greater than a quarter or equal to it
if (leftOver >= 25){
// Checking if we have enough available quarters to give for change
if (((int) (leftOver / 25)) <= numQuarters){
quarterToGive = (int) (leftOver / 25);
}
// if we don't have enough quarters, give whatever is left
else{
quarterToGive = numQuarters;
}
// update the amount we have to find change for
leftOver -= (quarterToGive * 25);
}
// Checking how many dimes to give if change is greater than a quarter or equal to it
if (leftOver >= 10){
// Checking if we have enough available dimes to give for change
if (((int) (leftOver / 10)) <= numDimes){
dimeToGive = (int) (leftOver / 10);
}
// if we don't have enough dimes, give whatever is left
else{
dimeToGive = numDimes;
}
// update the amount we have to find change for
leftOver -= (dimeToGive * 10);
}
// Checking how many nickels to give if change is greater than a quarter or equal to it
if (leftOver >= 5){
// Checking if we have enough available nickels to give for change
if (((int) (leftOver / 5)) <= numNickels){
nickelToGive = (int) (leftOver / 5);
}
// if we don't have enough nickels, give whatever is left
else{
nickelToGive = numNickels;
}
// update the amount we have to find change for
leftOver -= (nickelToGive * 5);
}
// If we are able to give the correct amount of change with the available coins return true
if (leftOver == 0){
// Saving coin output details in the output string
sprintf(outputString, COINS_RETURNED, quarterToGive, dimeToGive, nickelToGive);
// Updating the number of available coins in the machine
numQuarters += insertedQuarter - quarterToGive;
numDimes += insertedDime - dimeToGive;
numNickels += insertedNickel - nickelToGive;
return true;
}
// If unable to give correct amount of change return false
else{
return false;
}
}
// Function that takes in money in cents and return its value in dollar
double priceInDollar(int cents){
return (double)(cents / ((double) 100));
}
//******************************** Arduino Loop Function ****************************************//
// Empty Loop Function since it's not needed
void loop() {}