/*
Author : Oliver D. Feronel
Date : 03/08/2024
03/08/2024 - Initial release
Added LCD display and button debounce
Added credit system
Added dispensing function
Added proximity sensor
Added interrupt for coin slot
Added formatting for LCD display
Added delay for LCD display
05/20/25
-- Auto reset after 1 min if credit < 5 php
05/27/25
-- Auto stop motor if spin > set maxSpinTime
*/
/////////////////////////////// Simulator instruction ////////////////////////////////////
/*
1st : add credit via white button represent the coin slot, 5php is required change it in
variable "productPrice" if wanted
2nd : once credit is ok press any to the dispense button "Bx_DISPENSE" spinner led lights on
represent that motor has power and spinning
3rd : assuming product has fallin manualy pressed "Bx_PROX" coresponsing the "Bx_DISPENSE"
that been pressed
*/
#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
// I2C LCD setup
LiquidCrystal_I2C lcd(0x27, 20, 4);
/////////////////////////////// Pin and variables declration ///////////////////////////////
/*
Bx = button
Mx = motor
Px = product
PROX = proximity sensor
DISPENSE = dispense button
*/
#define COIN_SLOT 2 // Use interrupt pin
#define B1_DISPENSE 3
#define B2_DISPENSE 4
#define B3_DISPENSE 5
#define P1_PROX 6
#define P2_PROX 7
#define P3_PROX 8
// use analog pin for motor incase of using PWM
#define M1_SPINNER A0
#define M2_SPINNER A1
#define M3_SPINNER A2
// Debounce variables
unsigned long lastDebounceTime1 = 0;
unsigned long lastDebounceTime2 = 0;
unsigned long lastDebounceTime3 = 0;
const unsigned long debounceDelay = 50; // Debounce delay in milliseconds
unsigned long lastTriggerTime = 0;
const unsigned long MaxWaitingTime = 60000; // 60,000 milliseconds = 1 minute
unsigned long lastMotorSpin = 0;
const unsigned long maxSpinTime = 50000; // 50 seconds
int motor;
volatile unsigned long last_coin_inserted = 0;
const unsigned long interval = 500;
int curr_coin = 0;
int prev_coin = 0;
// Button state variables
bool buttonState1 = HIGH;
bool lastButtonState1 = HIGH;
bool buttonState2 = HIGH;
bool lastButtonState2 = HIGH;
bool buttonState3 = HIGH;
bool lastButtonState3 = HIGH;
// Interupt variables
volatile int credit = 0;
volatile bool justCredit = false;
// Global variables
int productPrice = 5; // Change to desire price
bool dispensing = false;
char lcdRowOne[21] = "FACEMASK VENDO"; // 20 characters + null terminator
char lcdRowTwo[21] = "";
char lcdRowThree[21] = "";
char lcdRowFour[21] = "";
int maxRowChar = 20;
////////////////////////////////////// End of variables declration ///////////////////////////////////////////////////////
// Count String Length & Add Spaces
void formatText(char *output, const char *text, char func)
{
int stringLength = strlen(text);
if (stringLength > maxRowChar)
{
strcpy(output, "Error: Max Length");
return;
}
int remainSpaceCount = maxRowChar - stringLength;
char makeSpace[maxRowChar + 1] = {0};
if (func == 'c')
{ // Center the text
int leftSpace = remainSpaceCount / 2;
int rightSpace = remainSpaceCount - leftSpace;
memset(makeSpace, ' ', leftSpace);
strcpy(output, makeSpace);
strcat(output, text);
memset(makeSpace, ' ', rightSpace);
strcat(output, makeSpace);
}
else if (func == 'f')
{ // Add spaces in front
memset(makeSpace, ' ', remainSpaceCount);
strcpy(output, makeSpace);
strcat(output, text);
}
else if (func == 'b')
{ // Add spaces in back
strcpy(output, text);
memset(makeSpace, ' ', remainSpaceCount);
strcat(output, makeSpace);
}
}
// Update LCD Display
void updateLcd()
{
char formattedText[21];
lcd.setCursor(0, 0);
formatText(formattedText, lcdRowOne, 'c');
lcd.print(formattedText);
lcd.setCursor(0, 1);
formatText(formattedText, lcdRowTwo, 'b');
lcd.print(formattedText);
lcd.setCursor(0, 2);
formatText(formattedText, lcdRowThree, 'b');
lcd.print(formattedText);
lcd.setCursor(0, 3);
formatText(formattedText, lcdRowFour, 'b');
lcd.print(formattedText);
strcpy(lcdRowTwo, " ");
strcpy(lcdRowThree, " ");
strcpy(lcdRowFour, " ");
}
// Dispense functions
void dispenseFunction1()
{
if (credit < productPrice)
{
Serial.println("Insufficient ");
strcpy(lcdRowTwo, "Insufficient credit");
// Update the LCD
updateLcd();
delay(2000);
return;
}
lcd.setCursor(0, 1);
lcd.print("Dispensing 1 ");
Serial.println("Dispensing 1 ");
digitalWrite(M1_SPINNER, HIGH);
motor = M2_SPINNER;
lastMotorSpin = millis(); // Add this line
dispensing = true;
Serial.println("Waiting for P1_PROX to detect");
strcpy(lcdRowFour, "Dispensing at P1");
// Update the LCD
updateLcd();
}
void dispenseFunction2()
{
if (credit < productPrice)
{
Serial.println("Insufficient ");
strcpy(lcdRowTwo, "Insufficient credit");
// Update the LCD
updateLcd();
delay(2000);
return;
}
lcd.setCursor(0, 1);
lcd.print("Dispensing 2 ");
Serial.println("Dispensing 2 ");
digitalWrite(M2_SPINNER, HIGH);
motor = M2_SPINNER;
lastMotorSpin = millis(); // Add this line
dispensing = true;
Serial.println("Waiting for P2_PROX to detect");
strcpy(lcdRowFour, "Dispensing at P2");
// Update the LCD
updateLcd();
}
void dispenseFunction3()
{
if (credit < productPrice)
{
Serial.println("Insufficient ");
strcpy(lcdRowTwo, "Insufficient credit");
// Update the LCD
updateLcd();
delay(2000);
return;
}
lcd.setCursor(0, 1);
lcd.print("Dispensing 3 ");
Serial.println("Dispensing 3 ");
digitalWrite(M3_SPINNER, HIGH);
motor = M3_SPINNER;
lastMotorSpin = millis(); // Add this line
dispensing = true;
Serial.println("Waiting for P1_PROX to detect");
strcpy(lcdRowFour, "Dispensing at P3");
// Update the LCD
updateLcd();
}
void addCredit()
{
credit += 1;
justCredit = true;
last_coin_inserted = millis();
}
void debounceButton()
{
// Read button states
bool reading1 = digitalRead(B1_DISPENSE);
bool reading2 = digitalRead(B2_DISPENSE);
bool reading3 = digitalRead(B3_DISPENSE);
// Debounce logic for B1_DISPENSE
if (reading1 != lastButtonState1)
{
lastDebounceTime1 = millis();
}
if ((millis() - lastDebounceTime1) > debounceDelay)
{
if (reading1 != buttonState1)
{
buttonState1 = reading1;
if (buttonState1 == LOW)
{
dispenseFunction1();
}
}
}
lastButtonState1 = reading1;
// Debounce logic for B2_DISPENSE
if (reading2 != lastButtonState2)
{
lastDebounceTime2 = millis();
}
if ((millis() - lastDebounceTime2) > debounceDelay)
{
if (reading2 != buttonState2)
{
buttonState2 = reading2;
if (buttonState2 == LOW)
{
dispenseFunction2();
}
}
}
lastButtonState2 = reading2;
// Debounce logic for B3_DISPENSE
if (reading3 != lastButtonState3)
{
lastDebounceTime3 = millis();
}
if ((millis() - lastDebounceTime3) > debounceDelay)
{
if (reading3 != buttonState3)
{
buttonState3 = reading3;
if (buttonState3 == LOW)
{
dispenseFunction3();
}
}
}
lastButtonState3 = reading3;
}
void reset_timer()
{
unsigned long currentTime = millis();
if (currentTime - lastTriggerTime >= MaxWaitingTime)
{
lastTriggerTime = currentTime;
dispensing = false;
credit = 0;
// Code to run every 1 minute
Serial.println("Rest Credit");
}
}
void reset_motor()
{
if (dispensing)
{
unsigned long currentTime = millis();
if (currentTime - lastMotorSpin >= maxSpinTime)
{
digitalWrite(motor, LOW); // Stop the motor
dispensing = false; // Reset dispensing flag
credit = 0; // Clear credit
Serial.println("Motor Force Stop");
}
}
}
void credit_fixer()
{
unsigned long currentTime_checker = millis();
if (currentTime_checker - last_coin_inserted >= interval)
{
if (credit - prev_coin == 4)
{
credit = prev_coin + 5;
justCredit = true;
}
prev_coin = credit;
last_coin_inserted = currentTime_checker;
}
}
bool inRange(int value, int min, int max)
{
return value >= min && value <= max;
}
void setup()
{
Serial.begin(115200);
// Initialize LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Facemask Vendo ");
// Set button pins type
pinMode(B1_DISPENSE, INPUT_PULLUP);
pinMode(B2_DISPENSE, INPUT_PULLUP);
pinMode(B3_DISPENSE, INPUT_PULLUP);
pinMode(P1_PROX, INPUT_PULLUP);
pinMode(P2_PROX, INPUT_PULLUP);
pinMode(P3_PROX, INPUT_PULLUP);
pinMode(M1_SPINNER, OUTPUT);
pinMode(M2_SPINNER, OUTPUT);
pinMode(M3_SPINNER, OUTPUT);
pinMode(COIN_SLOT, INPUT_PULLUP);
// activete interupt at D2
attachInterrupt(digitalPinToInterrupt(COIN_SLOT), addCredit, FALLING);
updateLcd();
Serial.println("System ready");
}
void loop()
{
debounceButton();
if (justCredit)
{
Serial.println("Credit : " + String(credit));
justCredit = false;
}
if ((!digitalRead(P1_PROX) || !digitalRead(P2_PROX) || !digitalRead(P3_PROX)) && dispensing)
{
digitalWrite(M1_SPINNER, LOW);
digitalWrite(M2_SPINNER, LOW);
digitalWrite(M3_SPINNER, LOW);
dispensing = false;
credit -= productPrice;
Serial.println("Done dispensing");
strcpy(lcdRowTwo, "Done dispensing");
// Update the LCD
updateLcd();
delay(2000);
}
// Usage
// if (inRange(credit, 1, 4)) reset_timer(); // Remove function as per customer not user friedly
if (!dispensing)
{
String creditMessage = "";
String productPriceMessage = "";
if (!credit)
{
// Create a String object to hold the concatenated message
creditMessage = "INSERT COIN";
strcpy(lcdRowThree, "Accepting 1 php");
strcpy(lcdRowFour, "Accepting 5 php");
}
else
{
// Create a String object to hold the concatenated message
creditMessage = "Credit : " + String(credit) + " php";
}
// Copy the String to a char array using .c_str()
strcpy(lcdRowTwo, creditMessage.c_str());
// Update the LCD
updateLcd();
}
else
{
reset_motor();
}
credit_fixer();
}