Critique Request: FMS-Based Vending Carwash Machine Sketch

Hello Everyone,

First off, a huge thanks to Alto777 and L8R for their invaluable feedback on my previous sketch and for guiding me on improving my code. Your input has been incredibly helpful!

I’m still relatively new to C++ and Arduino programming, and I’m currently working on developing a finite state machine (FSM)-based vending self-service car wash machine. The goal is to create a system capable of dispensing water and foam within specified time intervals, with a play/pause feature for enhanced user control. Assuming that I’m on the right track, I’d love some feedback on my approach to further improve my sketch and ensure the design is efficient and error-free. I have yet to incorporate the ezButton library into my sketch.

I've replaced the universal coin slot temporarily with buttons with pull-down resistors;
Orange wire button = Add 1 to the balance,
Yellow wire button = Add 5 to the balance,
Green wire button = Add 10 to the balance,
Blue wire button = Pump water with play/pause feature,
Purple wire button = Pump foam with play/pause feature;

In summary, I am trying to use Arduino Uno R3 to control an electrical outlet operating at 220V which a pressure washer would be plugged in and solenoid valve/s via a relay module.

This is the reference of what I am trying to do:
https://youtu.be/9qqWdO3FCd4?si=3Ye_tw3Ca87vhiYM

Please note that there may still be some bugs that I haven’t discovered and fixed. Furthermore, I am currently doing this project in WOKWI simulation only and I have yet to proceed in building the actual hardware system. I am afraid of burning Arduino Uno R3 and relay modules if I proceed without caution and proper knowledge.

Looking forward to your insights!

This is my FMS Diagram:

This is my circuit diagram and Wokwi project link:
https://wokwi.com/projects/418858256122530817

This is my sketch:

// C++ code

# include <LiquidCrystal_I2C.h>

# define I2C_ADDR    0x27
# define LCD_COLUMNS 16
# define LCD_LINES   2

int balance = 0;
const int cost = 5;

// Pin for relay, to be connected to solenoid valve and solid state relay?
int relayPin_Motor = 9;
int relayPin_SolenoidValve = 10;


// Event flag for water and foam feature
bool isWatering = false;
bool isFoaming = false;
bool hasWaterTime = false;
bool hasFoamTime = false;

// Variables for toggle
int buttonState;
int prevButtonState = 0;
byte buttonPins[5] = {2, 3, 4, 5, 6};
byte buttonStates[5];
byte prevButtonStates[5] = {0, 0, 0, 0, 0};
unsigned long debounceStart[5] = {0, 0, 0, 0, 0};
const unsigned long debounceDelay = 50;

// Millis() variables
unsigned long currentMillis;
unsigned long startWaterMillis;
unsigned long startFoamMillis;
const unsigned long second = 1000UL;
const unsigned long minute = second * 60;
const unsigned long waterTime = minute / 5;
const unsigned long foamTime = minute / 5;  
unsigned long elapsedWaterTime;
unsigned long elapsedFoamTime;
unsigned long waterTimeLeft = 0;
unsigned long foamTimeLeft = 0;
long displayWaterTime = 0;
long displayFoamTime = 0;

// Enum variable for the machine state
enum MachineState {IDLE, SELECTION, WATER, FOAM};
MachineState state;

LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);

void setup()
{
  // Serial
  Serial.begin(115200);

  //Intialize the LCD
  lcd.init();

  //Set the pin of coinButtons
  // Set the pin of water and foam button
  for(int buttonPin: buttonPins)
  {
    pinMode(buttonPin, INPUT);
  }
  
  // Set the pin of relay
  pinMode(relayPin_Motor, OUTPUT);
  pinMode(relayPin_SolenoidValve, OUTPUT);
  
  // Set the default state of the machine
  state = IDLE;
}

void loop()
{
  currentMillis = millis();
  ReadDebounceButton();
  HandleMachineState();
}

// Handle the states and transitions of the machine
void HandleMachineState()
{
  switch(state)
  {
    case IDLE:
    // Transition to SELECTION state if balance is greater than or equal to the cost of water or foam
    if(balance >= cost)
    {
      state = SELECTION;
    }
    else if(balance < cost)
    {
      if(hasWaterTime || hasFoamTime)
      {
        // Transition to SELECTION state if either water or foam has time left
        state = SELECTION;
      }
      else
      {
        // Display insert coins instruction
        // Display the current balance
        DisplayInsert();
        DisplayBalance(balance);
      }
    }
    break;
    
    case SELECTION:
    // Display press a button instruction
    // Display the current balance
    if(!hasWaterTime && !hasFoamTime)
    {
      DisplayPress();
      DisplayBalance(balance);
    }
    else
    {
      // Display the time left of the water and foam if either has any
      // Display the current balance
      DisplayMinSec(0, 0, "WTR", displayWaterTime);
      DisplayMinSec(9, 0, "FM", displayFoamTime);
      DisplayBalance(balance);
    }
    break;
    
    // If condition?
    case WATER:
    PumpWater();
    DisplayBalance(balance);
    break;
    
    case FOAM:
    PumpFoam();
    DisplayBalance(balance);
    break;
  }
}

// Read and debounce the water button
// TO-DO: Convert this method to be reused with different buttons in the circuit
// As buttons results to different output, converting this to reusable debounce button method is a challenge
// Abstract the switch case for different outputs?
void ReadDebounceButton(){
  for(int currentButton = 0; currentButton < sizeof(buttonPins); currentButton++)
  {
    int reading = digitalRead(buttonPins[currentButton]);

    if (reading != prevButtonStates[currentButton]) {
      debounceStart[currentButton] = currentMillis;
    }

    if ((currentMillis - debounceStart[currentButton]) > debounceDelay) {
      if (reading != buttonStates[currentButton]) {
        buttonStates[currentButton] = reading;
        if (buttonStates[currentButton] == 1) {
          lcd.clear();
          switch(buttonPins[currentButton])
          {
            case 2:
            Serial.println(F("1-peso Button Pressed"));
            balance += 1;
            break;
            
            case 3:
            Serial.println(F("5-pesos Button Pressed"));
            balance += 5;
            break;

            case 4:
            Serial.println(F("10-pesos Button Pressed"));
            balance += 10;
            break;
            
            case 5:
            Serial.println(F("Water Button Pressed"));
            WaterButtonOutputs();
            break;
            
            case 6:
            Serial.println(F("Foam Button Pressed"));
            FoamButtonOutputs();
            break;
            
            default:
            Serial.println(F("No button was read"));
          }
        }
      }
    }
    prevButtonStates[currentButton] = reading;
  }
} 

// Handles the diffrent outputs of pressed water button based on the current state of the machine
void WaterButtonOutputs()
{
  switch(state)
  {
    case IDLE:
    DisplayInsufficient();
    // TO-DO: Search for better solution instead of using delay in lcd displa, use millis() instead?
    delay(second / 2);
    break;

    case SELECTION:
    if(balance >= cost)
    {
      if(!hasWaterTime)
      {
        balance -= cost;
        waterTimeLeft = waterTime;
        startWaterMillis = currentMillis;
        isWatering = true;
        hasWaterTime = true;
        state = WATER;
      }
    }
    else if(balance < cost)
    {
      if(hasWaterTime)
      {
        TogglePumpWater();
        state = WATER;
      }
      else
      {
        DisplayInsufficient();
        delay(second / 2);
        lcd.clear();
      }
    }
    
    break;

    case WATER:
    TogglePumpWater();
    PumpWater(); // Call the PumpWater again to turn off the relay before transitioning to SELECTION state
    state = SELECTION;
    break;

    case FOAM:
    TogglePumpFoam();
    PumpFoam(); // Call the PumpFoam again to turn off the relay before transitioning to WATER state

    if(balance >= cost)
    {
      if(!hasWaterTime)
      {
        balance -= cost;
        waterTimeLeft = waterTime;
        startWaterMillis = currentMillis;
        isWatering = true;
        hasWaterTime = true;
        state = WATER;
      }
    }
    else if(balance < cost)
    {
      if(hasWaterTime)
      {
        TogglePumpWater();
        state = WATER;
      }
      else
      {
        DisplayInsufficient();
        delay(second / 2);
        lcd.clear();
        state = IDLE;
      }
    }
    
    break;

    default:
    state = IDLE;
  }
}

// Handles the diffrent outputs of pressed foam button based on the current state of the machine
void FoamButtonOutputs()
{
  switch(state)
  {
    case IDLE:
    DisplayInsufficient();
    // TO-DO: Search for better solution instead of using delay in lcd displa, use millis() instead?
    delay(second);
    break;

    case SELECTION:
    if(balance >= cost)
    {
      if(!hasFoamTime)
      {
        balance -= cost;
        foamTimeLeft = foamTime;
        startFoamMillis = currentMillis;
        isFoaming = true;
        hasFoamTime = true;
        state = FOAM;
      }
    }
    else if(balance < cost)
    {
      if(hasFoamTime)
      {
        TogglePumpFoam();
        state = FOAM;
      }
      else
      {
        DisplayInsufficient();
        delay(second / 2);
        lcd.clear();
      }
    }
    
    break;

    case WATER:
    TogglePumpWater();      
    PumpWater(); // Call the PumpWater again to turn off the relay before transitioning to FOAM state

    if(balance >= cost)
    {
      if(!hasFoamTime)
      {
        balance -= cost;
        foamTimeLeft = foamTime;
        startFoamMillis = currentMillis;
        isFoaming = true;
        hasFoamTime = true;
        state = FOAM;
      }
    }
    else if(balance < cost)
    {
      if(hasFoamTime)
      {
        TogglePumpFoam();
        state = FOAM;
      }
      else
      {
        DisplayInsufficient();
        delay(second / 2);
        lcd.clear();
        state = IDLE;
      }
    }
    break;

    case FOAM:
    TogglePumpFoam();
    PumpFoam(); // Call the PumpFoam again to turn off the relay before transitioning to SELECTION state
    state = SELECTION;
    break;

    default:
    state = IDLE;
  }
}

// Method to pump water
void PumpWater()
{
  // long displayWaterTime; Global variable?
  if(isWatering)
  {
    elapsedWaterTime = currentMillis - startWaterMillis;
    if(elapsedWaterTime > waterTimeLeft)
    { 
      // Turn off the relay
      if(digitalRead(relayPin_Motor) == 1)
      {
        digitalWrite(relayPin_Motor, 0);
      }
      // Reset the water event flags
      // Transition to the IDLE state after the countdown
      isWatering = false;
      hasWaterTime = false;
    }
    else if(elapsedWaterTime <= waterTimeLeft)
    {
      // Turn on the relay
      if(digitalRead(relayPin_Motor) == 0)
      {
        digitalWrite(relayPin_Motor, 1);
      }
      displayWaterTime = waterTimeLeft - elapsedWaterTime;
    }
  }
  else if(!isWatering)
  {
    // This condition could not be read as the state transition to another state.
    // Problem: Could not turn off the relay during paused countdown.
    // Solution? Also call the PumpWater in selection state or after toggling? Split the pumpwater into on and off of water pump?
    // Turn off the relay
    if(digitalRead(relayPin_Motor) == 1)
    {
      digitalWrite(relayPin_Motor, 0);
    }

    // Update the displayWaterTime based on waterTimeLeft;
    if(hasWaterTime)
    {
      displayWaterTime = waterTimeLeft;
    }
  }
  
  // Display water time left
  // Display the foam time as well?
  if(hasWaterTime)
  {
    DisplayMinSec(0, 0, "WTR", displayWaterTime);
    DisplayMinSec(9, 0, "FM", displayFoamTime);
  }
  else if(!hasWaterTime)
  {
    // Reset the waterTimeLeft and displayWaterTime after the countdown
    waterTimeLeft = 0;
    displayWaterTime = 0;
    state = IDLE;
  }
}

// Play/Pause toggle of the pump water and water time countdown:
void TogglePumpWater()
{
  isWatering = !isWatering;
  if(isWatering)
  {
    // Update the start millis of water countdown
    startWaterMillis = currentMillis;
    Serial.println(F("WATER IS RUNNING"));
  }
  else if(!isWatering)
  {
    // Update the water time left
    waterTimeLeft -= elapsedWaterTime;
    Serial.println(F("WATER HAS STOPPED"));
  }
}

// Method to pump foam
void PumpFoam()
{
  // long displayFoamTime; Global variable?
  if(isFoaming)
  {
    elapsedFoamTime = currentMillis - startFoamMillis;
    if(elapsedFoamTime > foamTimeLeft)
    { 
      // Turn off the relay
      if(digitalRead(relayPin_SolenoidValve) == 1)
      {
        digitalWrite(relayPin_SolenoidValve, 0);
      }
      // Reset the water event flags
      // Transition to the IDLE state after the countdown
      isFoaming = false;
      hasFoamTime = false;
    }
    else if(elapsedFoamTime <= foamTimeLeft)
    {
      // Turn on the relay
      if(digitalRead(relayPin_SolenoidValve) == 0)
      {
        digitalWrite(relayPin_SolenoidValve, 1);
      }
      displayFoamTime = foamTimeLeft - elapsedFoamTime;
    }
  }
  else if(!isFoaming)
  {
    // This condition could not be read as the state transition to another state.
    // Problem: Could not turn off the relay during paused countdown.
    // Solution? Also call the PumpFoam in selection state or after toggling? Split the pump foam into on and off of foam pump?
    // Turn off the relay
    if(digitalRead(relayPin_SolenoidValve) == 1)
    {
      digitalWrite(relayPin_SolenoidValve, 0);
    }

    // Update the displayFoamTime based on foamTimeLeft;
    if(hasFoamTime)
    {
      displayFoamTime = foamTimeLeft;
    }
  }
  
  // Display water time left
  // Display the foam time as well?
  if(hasFoamTime)
  {
    DisplayMinSec(0, 0, "WTR", displayWaterTime);
    DisplayMinSec(9, 0, "FM", displayFoamTime);
  }
  else if(!hasFoamTime)
  {
    // Reset the waterTimeLeft and displayWaterTime after the countdown
    foamTimeLeft = 0;
    displayFoamTime = 0;
    state = IDLE;
  }
}

// Play/Pause toggle of the pump foam and foam time countdown:
void TogglePumpFoam()
{
  isFoaming = !isFoaming;
  if(isFoaming)
  {
    // Update the start millis of water countdown
    startFoamMillis = currentMillis;
    Serial.println(F("FOAM IS RUNNING"));
  }
  else if(!isFoaming)
  {
    // Update the water time left
    foamTimeLeft -= elapsedFoamTime;
    Serial.println(F("FOAM HAS STOPPED"));
  }
}

// Display the milliseconds in minutes:seconds format
void DisplayMinSec(int h, int v, String s, long time)
{
  lcd.setCursor(h, v);
  lcd.print(s);
  int countdown_minute = ((time / 1000) / 60) % 60;
  int countdown_sec = (time / 1000) % 60;
    
  if(countdown_minute < 10)
  {
    lcd.print(' ');
  }
  lcd.print(countdown_minute);
  lcd.print(':');
  if(countdown_sec < 10)
  {
    lcd.print('0');
  }
  lcd.print(countdown_sec);
}

// Display the current balance, parameter?
void DisplayBalance(int bal)
{
  lcd.setCursor(0, 1);
  lcd.print("  BALANCE: P");
  lcd.print(bal);
}

// Display the coin insert instruction
void DisplayInsert()
{
  lcd.home();
  lcd.print("INSERT:P1,P5,P10");
}

// Display the press a button instruction
void DisplayPress()
{
  lcd.home();
  lcd.print(" PRESS A BUTTON ");
}

// Display the insufficient balance instruction
void DisplayInsufficient()
{
  lcd.home();
  lcd.print(" INSUFF BALANCE ");
}

Thank you very much in advance!

seems like a lot of hard to maintain code.

in general, an FSM is driven by events. Those events can be generic, that is applicable to more than one state, a pertinent example is tmr that has expired.

while actions can depend on state, they more frequently depend on the transitions between states.

so i'm confused why there is a WaterButtonOutputs() and why such a function would need to know about balance and cost. Same is true for FoamButtonOutputs().

i don't understant the need for you blue/water and purple/foam buttons. i can understand a pause and possibly reset buttons

i would expect to see a state machine that sequentially sequences thru transitions that apply water and foam for specific periods of time.

it's not clear what you're code is intended to do. wondering about the water/foam buttons, i'm wondering if your machine, once enabled by a sufficient balance, allows a limited amount (time) of water as well as a limited amount of foam to be used, and possibly limited total amount of time before becoming disabled

Thank you for your input. It seems I didn't fully understand what is FSM. I'll be studying FSM again for vending machine, to fully understand it's concept. I apologize for the lack of information and ambiguity. What I am trying to program is a vending self-service car wash machine. Please refer to the YouTube link above that I've just added:
https://youtu.be/9qqWdO3FCd4?feature=shared

Your assumption is correct about the water and foam buttons. Once there is a sufficient balance, by pressing the water or foam button. The vending machine would start pumping either water or foam for limited amount of time. The water/foam button could also be used to stop the pumping of either the water or foam as well pause it's corresponding timer.

Your description leaves room for interpretation. Your code may be doing what you want, it is hard to tell and at a glance looks more complicate and involved than might need be.

Post a link to your wokwi simulation, then continue your work on a copy so we can have a go at trying what you have working at the moment.

Please also write a brief coherent description, maybe make it like the quick start page for the owner's manual for this device.

I'll try:

[Although as I write this it seems like the foam button should dispense an amount of foam per button press. Charging accordianly. But this is your project.]

I don't understand the arrows in your diagram that show the water and foam buttons making transitions between watering and, um, foaming.

And what happens if we finish and there is a balance left over? Does the time we take using water and foam enter into the cost? Should the balance be simply forfeited after some period of inactivity?

a7

Thank you very much, I'll be working on my description and manual, your brief description mostly got what I am trying to do. This is the link of my Wokwi project, it's also in the post above at the top of the circuit diagram:
https://wokwi.com/projects/418858256122530817

Regarding the transition between the water and foam state, I just thought that it would be essential to be able to transition between the water and foam state without needing to transition to the selection state. This results to while the machine is in water state, the foam button can stop the water and pause the corresponding timer then starts the foam afterwards. This applies both ways.

look this over
certainly not trivial
helps to separate monitor input and timers
helps to have distinct events in each state
this doesn't drive the outputs, just text testing the logic

// automated hand car-wash

const byte PinWater = 12;
const byte PinFoam  = 11;

enum { OutOff = HIGH, OutOn = LOW };      // depends on wiring

char s [90];

// -----------------------------------------------------------------------------
// common output routine, can also be used to output to LCD
void
disp (
    const char *s )
{
    Serial.println (s);
}

// -----------------------------------------------------------------------------
// monitor all inputs, return index of input when pressed
//    or return NoInpt
const byte PinInp [] = { A1, A2, A3 };
const byte Ninp       = sizeof(PinInp);
const int NoInp       = -1;

byte inpState [Ninp];

enum { Water, Foam ,Coin_5, Coin_1, Coin_10};   // identifies pins

int
chkInp ()
{
    for (int n = 0; n < Ninp; n++)  {
        byte inp = digitalRead (PinInp [n]);

        if (inpState [n] != inp)  {         // state change
            inpState [n] = inp;
            delay (10);                     // debounce

            if (LOW == inp)                 // pressed
                return n;
        }
    }
    return NoInp;
}

// -----------------------------------------------------------------------------
// add to the balance depending on coin and return balance
const int MinBalance = 20;
int balance;

int
updateBalance (
    int coin )
{
    switch (coin) {
    case Coin_1:
        balance += 1;
        break;

    case Coin_5:
        balance += 5;
        break;

    case Coin_10:
        balance += 10;
        break;

    default:                // ignore non-coin input
        return balance; 
        break;
    }

    sprintf (s, " balance %3d", balance);
    disp (s);

    return balance;
}

// -----------------------------------------------------------------------------
// state and event (inputs and tmr) state machine
enum { Off, Idle, WaterOn, FoamOn };
int state = Off;

// timers that track system on and water/foam usage times
struct Tmr {
    const unsigned long MsecPeriodMax;  // max period
    const char   *label;                // text label

    bool          active;
    unsigned long msec;
    unsigned long msecPeriod;           // remaining water/foam  period
}
tmr [] = {
    {  5000, "water"  },
    {  2000, "foam"   },
    { 30000, "system" },
};
const int Ntmr = sizeof(tmr)/sizeof(Tmr);

enum { T_Water, T_Foam, T_System };     // indices of timers

// -------------------------------------
void
loop (void)
{
    // -------------------------------------
    // handle timers
    unsigned long msec = millis ();

    for (int n = 0; n < Ntmr; n++)  {
        if (tmr [n].active)  {
            if (msec - tmr [n].msec >= tmr [n].msecPeriod) {
                tmr [n].active     = false;         // expired
                tmr [n].msecPeriod = 0;             // indicates time-out
            }
        }
    }

    // -------------------------------------
    // handle inputs and state
    int inp = chkInp ();

    // state machine containing checks for events
    switch (state)  {
    case Off:
        digitalWrite (PinWater, OutOff);
        digitalWrite (PinFoam,  OutOff);

        if (MinBalance <= updateBalance (inp))  {
            balance -= MinBalance;

            // reset timers & enable system timer
            for (int n = 0; n < Ntmr; n++)
                tmr [n].msecPeriod = tmr [n].MsecPeriodMax;
            tmr [T_System].active = true;
            tmr [T_System].msec   = msec;

            state    = Idle;
            disp ("System On");
        }
        break;

    // system on, water/foam off
    case Idle:
        if (0 == tmr [T_System].msecPeriod)  {
            state = Off;
            disp ("System time-out");
        }
        else if (Water == inp)  {
            if (0 < tmr [T_Water].msecPeriod)  {
                digitalWrite (PinWater, OutOn);
                tmr [T_Water].active = true;
                tmr [T_Water].msec   = msec;
                state = WaterOn;
                sprintf (s, "Water On - time remaining %lu",
                        tmr [T_Water].msecPeriod);
                disp (s);
            }
            else
                disp ("Water timed-out");
        }
        else if (Foam == inp && 0 < tmr [T_Foam].msecPeriod)  {
            if (0 < tmr [T_Foam].msecPeriod)  {
                digitalWrite (PinFoam, OutOn);
                tmr [T_Foam].active = true;
                tmr [T_Foam].msec   = msec;
                state = FoamOn;
                sprintf (s, "Foam On - time remaining %lu",
                        tmr [T_Foam].msecPeriod);
                disp (s);
            }
            else
                disp ("Foam timed-out");
        }
        break;

    // system on, foam on
    case FoamOn:
        if (0 == tmr [T_System].msecPeriod)  {
            state = Off;
            disp ("System time-out");
        }
        else if (0 == tmr [T_Foam].msecPeriod)  {
            state = Idle;
            disp ("Foam time-out");
        }
        else if (Foam == inp)  {
            digitalWrite (PinFoam, OutOff);
            tmr [T_Foam].active      = false;
            tmr [T_Foam].msecPeriod -= msec - tmr [T_Foam].msec;
            state = Idle;
            sprintf (s, "Foam Off - time remaining %lu",
                    tmr [T_Foam].msecPeriod);
            disp (s);
        }
        break;

    // system on, water on
    case WaterOn:
        if (0 == tmr [T_System].msecPeriod)  {
            state = Off;
            disp ("System time-out");
        }
        else if (0 == tmr [T_Water].msecPeriod)  {
            state = Idle;
            disp ("Water time-out");
        }
        else if (Water == inp)  {
            digitalWrite (PinWater, OutOff);
            tmr [T_Water].active      = false;
            tmr [T_Water].msecPeriod -= msec - tmr [T_Water].msec;
            state = Idle;
            sprintf (s, "Water Off - time remaining %lu",
                    tmr [T_Water].msecPeriod);
            disp (s);
        }
        break;
    }
}

// -----------------------------------------------------------------------------
void
setup (void)
{
    Serial.begin (9600);

    for (int n = 0; n < Ninp; n++)  {
        pinMode (PinInp [n], INPUT_PULLUP);
        inpState [n] = digitalRead  (PinInp [n]);
    }

    digitalWrite (PinWater, OutOff);
    digitalWrite (PinFoam,  OutOff);
    pinMode (PinWater, OUTPUT);
    pinMode (PinFoam,  OUTPUT);

    disp ("Ready");
}
1 Like

Thank you very much for writing this sketch, I'll be studying your sketch and it's underlying logic and design.

So... I can't get foam and water at the same time?

You choose to have the foam button stop the water if it is running. Another choice might have been to stop the water and start the foam.

You choose to have the water button stop the foam if it is running. Another choice might have been to stop the foam and start the water.

a7

never mind... Closer reading makes me think you did intend that pressing either button would stop the other activity and start that button's activity.

I have the same dull headache I get whenever "looking over" @gcjr's code, but I thought it might be useful as an operational specification.

Sadly, it doesn't run on the hardware you built in the wokwi. So when you do get what it takes to make any use of his version, your first step might be to know it just well enough to be able to change what I hope will be obvious so it will run on the wokwi without changes to your wiring.

So some pins need to be reassigned, and the switches are designed to be LOW when pressed. This is usually the best choice; you could change your thinking to agree or make adjustments to the code where the buttons are read.

Something that can help is making the choice easy.

Here in @gcjr' code there is a way to "fix" relays that are operating opposite to plan:

enum { OutOff = HIGH, OutOn = LOW };      // depends on wiring

You could do the same thing for buttons. Up top with the other stuff write:

# define PRESSED  HIGH
# define NOT_PRESSED  LOW

Then everywhere you read buttons, use those manifest constants

  if (digitalRead(someButton) == PRESSED) {
// whatever you needa do when the button is pressed

and in that way provide the same easy flexibility to how the buttons happen to be wired.

Ideally, a sketch might only have the literal appearances of LOW and HIGH up at the top defining constants; it makes not only for easy changes but more readable code. No one has to go looking at your relay module or how it is wired if you write

  digitalWrite(relayPin_Motor, OFF);

and as a benefit you can lose what would thus become the gratuitous comment

// Turn off the relay

In a sketch this small, there should be almost zero need for comments, particularly if a good detailed description of what the project is meant to do is at hand.

a7

1 Like

try this carWash - Wokwi ESP32, STM32, Arduino Simulator

corrected

  • LED on/off state
  • water/foam time-out to turn off LED
1 Like

My thinking was that in FSM-based machine, the machine should only be at 1 state at a time and could not be at multiple states at the same time. Hence, when designing the FSM diagram I put the constraint that the water and foam could not be used at the same time. On second thought, I think it would be benificial if this states start and stop independently with each other. I'll be re-evaluating my FSM diagram and try to improve it after I've studied the FSM concept throughly.

Thank you very much for explaining the importance of input-pullup and the practices I should adhere to make my sketch more redeable and simple. I'll be take it into mind when refactoring my sketch after fully understanding Gcjr sketch and it's underlying logic and design.

Thank you very much for further incorporating your sketch into my Wokwi project. Your sketch made me realize how my sketch fare in coding standards and practices. I'll be studying the use of struct and it's difference with class. As well as minimizing the memory usage of Arduino Uno when programming and refactoring.

Thank you very much once again @alto777 and @gcjr, your feedback is very invaluable on how I can further improve my sketch and design. At first, I thought this project is as easy as controlling the corresponding relay. I am very grateful to learn so much from you guys and never expected how much I lack the necessary knowledge. Belated happy new year, may your year be nothing short of amazing!

I don't know if I mentioned the IPO model.

Read about it in the abstract here

The IPO Model

It just means read all the buttons, decide what you are doing and how the buttons change that, then set you outputs.

A FSM might be part of this, maybe not. But the system will certainly be in some state, and time and inputs will def change that state, and the state it is in and the inputs it has received will inform its outputs. So call it whatever you want.

Now I hoped @gcjr's sketch would function close enough to your hopes as to be useful for talking about it.

I cannot add coins except when the system is idle.

I can stop and start one period of water and one period of foam, but the water and foam stay on beyond the time I seem to have paid for.

There were a few other odd things, so I stopped.

Maybe those could all be fixed in the context of the code as presented, I have no time to see.

But it is valuable in that it underlines the importance of first getting down on paper the exact desired functionality, without thinking about the code and how the limits of coding might make you do one thing instead of another.

The limits are in the hardware you've chosen. I can practically guarantee that anything you think up as to "how it works" can be realized in the code if your hardware is capable of it. As the example at hand, if your pimp system can produce foam and water at the same time, so can the code make it do. And so forth.

I want to add money and have the water flow as long as I've paid for it to do.

I still woder if foam shouldn't be an amount per button press costing whatever it does, but I do remember car washes fro when I was allowed to drive and they have foam for time at a cost, so.

You the boss here - I look forward to your ridiculously detailed specifications…

a7

1 Like

i see the LEDs toggling on/off when the buttons are pressed, albeit opposite of what is intended

not specified how much more time, water-time and foam-time should be increased with each coin

it's often difficult to completely imagine what the "machinery" needs to do while drafting a design on paper, hence the value of a simulation to exercise the design and verify what was "imagined"

1 Like

I apologize for the lack of information in my description that's causing misinterpretation and ambiguity. I am currently re-evaluating my FSM diagram and creating a flow chart to have a better grasp of the procedure. There may be changes on my design, the vending machine is able to accept coins at any given time and record the cummulative inserted coins. I am currently considering two versions on how the vending machine would charge the user based on the cummumalive inserted coins.

The 1st one is, once the balance is sufficient. The vending machine would automatically charged the cost and proceed to the product selection (e.g. water or foam).

The 2nd one is, once the balance is sufficient. The vending machine would only charge the cost when the user pressed the corresponding button for either water or foam.

Afterwards, the pumping system will be powered on and start dispensing the selected product at a limited amount of time. The corresponding button could pause and resume the dispensing as well as the timer of the selected product. When paused, the pumping system will be powered off and be powered on once resumed, that is why the LEDs are toggling on/off when the corresponding button is pressed.

I'll be focusing for now in the design documentation and studying what you've guys pointed out before proceeding again to programming and refactoring. Thanks once again for your feedbacks and insights.

what i implemented

  • sufficient balance sets a limit on the total time, water time and foam time

  • the systems is disabled when the total time expired

  • the water and foam times limit the durations they can be on (and it would make sense that the system is disabled when both reach their limits)

  • i chose very limited times for testing

  • additional coins could add increments of time, if 5 coins allowed 150 secs of water, an additional coin could add 30 secs. Similar for foam and total time

1 Like

They do.

Oddly, changing your relay enum does not change the LEDs hooked to the relays, at least not at startup.

In any case, add 20 units, turn on the foam and wait... the foam times out, its relay does not change. Free foam!

Soon enough, the system times out (still don't know how/what @thecodeisbugged may want to mean by that) and the the foam rely reverts.

a7