Using millis() instead of delay in a FSM

Hello everybody,

I am currently working on an arduino project, and its purpose is to command relays that can stand very high voltage (1250+V) with the arduino.
Since I can’t directly command these relays with the arduino, I’ve used the module relays since they have octocoupler and are easy to use to do so.
So, I’ll command a relay from the relay module to turn ON or OFF another relay (by sending it 28V to the coil) that can stand higher voltage.

The problem is, I need to turn these relays ON and OFF during a certain amount of time. I’ve designed a final state machine (with switch case) so that some of the relays are ON for a certain amount of time, then turned off, and other relays will be ON for a certain amount of time…
To do so, I’ve used delay(); but it prevents me from doing anything else in the program : for example, I’d like to press a button that’ll stop everything without using an interruption, but it doesn’t process the code in the loop() since it is going through the delay.

That’s why I’ve started to think about how I could use millis() to do so, but I have no idea how it can be done in a FSM.

I’ll give you an example of two sequences that are done in the FSM :

switch(seq){

case 1 :

digitalWrite(relay3, RELAY_ON); // commanding the module relays
digitalWrite(relay4, RELAY_ON);
digitalWrite(relay5, RELAY_ON);
digitalWrite(relay6, RELAY_ON);
digitalWrite(HTp, RELAY_ON);
digitalWrite(HTm, RELAY_ON);
delay(Tchosen);
digitalWrite(HTp, RELAY_OFF);
digitalWrite(HTm, RELAY_OFF);
seq = 2;

case 2 :

digitalWrite(relay5, RELAY_OFF);
digitalWrite(relay6, RELAY_OFF);
digitalWrite(relay9, RELAY_ON);
digitalWrite(relay3, RELAY_ON);
digitalWrite(relay4, RELAY_ON);
digitalWrite(HTp, RELAY_ON);
digitalWrite(HTm, RELAY_ON);
delay(Tchosen);
digitalWrite(HTp, RELAY_OFF);
digitalWrite(HTm, RELAY_OFF);
seq = 3;

etc…

I’ve simplified the code greatly : it’s just the basics of what I want to do without having to use delay();

Thank you to anyone that’ll help me
Laëtitia

——

Edit : I’ve had many great answers that helped me :slight_smile: thanks for the reactivity - I wasn’t expecting so many useful answers.

What I usually do is have a flag, say newState which is true. Then you only do stuff at the top of each case in an if looking for newstate == true. Then you set that flag false so it only runs that stuff once. (That would be those lines up to the delay().

Also at the top of the case, capture the time say startedThisCase as millis().

Then in the body of the case, outside that first if, you check the ever increasing new value of millis() - the time the state started, against the “delay” value (which is no longer a delay() :wink: ) and do that last few lines including setting the state variable for the next one, and set set newstate true, so when the next case starts, it only does the top stuff once.

Something like this (pseudo code, syntax not perfect, typing straight into here not the ide)

case 1 :
if (newstate ==true)
{
   newstate=false;
startedthisstateat=millis();
    digitalWrite(relay3, RELAY_ON); // commanding the module relays
    digitalWrite(relay4, RELAY_ON);
    digitalWrite(relay5, RELAY_ON);
    digitalWrite(relay6, RELAY_ON);
   digitalWrite(HTp, RELAY_ON);
   digitalWrite(HTm, RELAY_ON);
}

if (millis() - startedthisstateat >= tchosen)
{
  newstate=true;
digitalWrite(HTp, RELAY_OFF);
   digitalWrite(HTm, RELAY_OFF);
   seq = 2;
}

break

Then it will zoom thru that case very fast, and any buttons checked outside of the switch…case in the “body” of the sketch will be honoured.

Sorry that explanation may seem a bit rushed… and rough

PS to above... here's a snippet from an actual sketch, that illustrates the above (different variable names etc). It's the same idea, just laid of a bit nicer from the ide:

switch (currentState)
  {
    
    case ST_primed:
      if (newlyArrivedInThisState)
      {
        startedTimingAt = millis();
        Serial.print("Arrived in state primed at ");
        Serial.print(startedTimingAt);
        newlyArrivedInThisState = false;
      }

      if (millis() - startedTimingAt >= firstInterval)
      {
        newlyArrivedInThisState = true;
        Serial.println("   ... leaving state primed to 1led");
        currentState = ST_1led;
      }

      break;

I would split the state with the delay into two states, maybe three. You can put the waiting in the first state or in the next state or in its own state in between. Since every state is seperated, just one global 'previousMillis' can be used for every wait.

For example:

switch(state):
{
  case 1:
    digitalWrite(relay3, RELAY_ON);
    digitalWrite(relay4, RELAY_ON);
    digitalWrite(relay5, RELAY_ON);
    digitalWrite(relay6, RELAY_ON);
    digitalWrite(HTp, RELAY_ON);
    digitalWrite(HTm, RELAY_ON);

    previousMillis = millis();  // prepare for waiting state
    state = 2;
    break;
  case 2:
    if(millis() - previousMillis >= Tchosen)
    {
      state = 3;
    }
    break;
  case 3:
    digitalWrite(HTp, RELAY_OFF);
    digitalWrite(HTm, RELAY_OFF);
    seq = 4;
    break;

There is no need to stop the waiting by doing something with 'previousMillis' or by adding a boolean variable. Once in state 3, the 'previousMillis' is no longer used.

I would add "unsigned long currentMillis = millis()" at the beginning of the loop() and use 'currentMillis' in the FSM. And I would use a 'enum' for the numbers of the states.

I favour adding states rather than having another variable to switch behavior in a state. If there's one time stuff to execute for a state, I do it in the predecessor state when I set the state variable to the new state.

This does mean that sometimes I need an extra "startup" state to handle clean entry into the first 'real' state.

The functions delay() and delayMicroseconds() block the Arduino until they complete. Have a look at how millis() is used to manage timing without blocking in Several Things at a Time.

And see Using millis() for timing. A beginners guide if you need more explanation.

And (because they also block the Arduino) don't use WHILE or FOR unless they complete very quickly - in a small number of microseconds. Generally it is better to use IF and allow loop() to do the repetition.

...R

Thank you so much guys :) I've used the method with more states that Koepel gave me, and I think it's perfect for what I want to do. You've all saved me a lot of time ! Laëtitia

When using a FSM, I like to gather all the information before the FSM. When blinking a led, then I would put a millis() timer outside the FSM and control it from the FSM. When the FSM is in a waiting state, then it is possible to check also for button. Those three things can be seen in my millis_and_finite_state_machine.ino example.

Once your sketch uses millis(), there is time to do an other tasks. You can even add more FSM's that do other tasks.

This is an example made to show how to remove delays (un-delay) from simple code using a finite state machine packaged into a function with a 1-shot millis timer. There is one timer for several wait-untils.

// add-a-sketch_un-delay 2018 by GoForSmoke @ Arduino.cc Forum
// Free for use, Apr 30/18 by GFS. Compiled on Arduino IDE 1.6.9.
// This sketch shows a general method to get rid of delays in code.
// You could upgrade code with delays to work with add-a-sketch.

#include <avr/io.h>
#include "Arduino.h"

const byte ledPin = 13;
unsigned long delayStart, delayWait;

void setup()
{
  Serial.begin( 115200 );
  Serial.println( F( "\n\n\n  Un-Delay Example, free by GoForSmoke\n" ));
  Serial.println( F( "This sketch shows how to get rid of delays in code.\n" ));

  pinMode( ledPin, OUTPUT );
};


/* The section of the original sketch with delays:
 * 
 * digitalWrite( ledPin, HIGH );   --  0
 * delay( 500 );
 * digitalWrite( ledPin, LOW );    --  1
 * delay( 250 );
 * digitalWrite( ledPin, HIGH );   --  2
 * delay( 250 );
 * digitalWrite( ledPin, LOW );    --  3
 * delay( 250 );
 * digitalWrite( ledPin, HIGH );   --  4
 * delay( 1000 );
 * digitalWrite( ledPin, LOW );    --  5
 * delay( 1000 );
 */

byte blinkStep; // state tracking for BlinkPattern() below

void BlinkPattern()
{
  // This one-shot timer replaces every delay() removed in one spot.  
  // start of one-shot timer
  if ( delayWait > 0 ) // one-shot timer only runs when set
  {
    if ( millis() - delayStart < delayWait )
    {
      return; // instead of blocking, the undelayed function returns
    }
    else
    {
      delayWait = 0; // time's up! turn off the timer and run the blinkStep case
    }
  }
  // end of one-shot timer

  // here each case has a timed wait but cases could change Step on pin or serial events.
  switch( blinkStep )  // runs the case numbered in blinkStep
  {
    case 0 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 0 doing something unspecified here at " ));
    Serial.println( delayStart = millis()); // able to set a var to a value I pass to function
    delayWait = 500; // for the next half second, this function will return on entry.
    blinkStep = 1;   // when the switch-case runs again it will be case 1 that runs
    break; // exit switch-case

    case 1 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 1 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 2;
    break;

    case 2 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 2 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 3;
    break;

    case 3 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 3 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 4;
    break;

    case 4 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 4 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 5;
    break;

    case 5 :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "Case 5 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 0;
    break;
  }
}


void loop()  // runs over and over, see how often
{            
  BlinkPattern();
}

I have no problems with nesting FSM’s or running them in parallel in non-blocking code.

Get rid of delay() and execution blocking code and generally your code will run many times faster than blocky code. A delay(1) wastes 16000 cycles when small tasks might run < 100 to < 400 cycles. I have posted examples where a void loop() counter showed 66700 loops per second unless the line

// delay(1);

is uncommented and then loop() runs 980 times a second.

Learn to write non-blocking code using time values and FSM’s is good. Make it as loop()-driven as possible, you can add loads of code to it.

wildbill:
If there’s one time stuff to execute for a state, I do it in the predecessor state when I set the state variable to the new state.

For work I had to learn assembly and they do precisely this. This is the most horrific abomination in coding I have ever seen. Followd by all the ‘inc state’ or state++; Also terror.

If you have to add a state between 2 other states, you have to copy-paste code from one state to another. This is ever susceptible to bugs.

I used one extra variable per state machine and what can I do? I can add states without having to copy-paste code. Furthermore I make good use of macros to make stuff more readable and to hide code.

State(someName) {
  entryState {
   // one time only stuff
  }
  onState { // continous stuff
     if (x < y) exitFlag = true; // setting this flags will run the ext state
  }
  exitState {
     // optional code to run once before exiting the state
     return true; 
  } 
}

This is one state ‘State(x)’ is a macro for ‘static bool xF(void)’.
It is a function which returns true when it is finished, the exitState macro returns false when exitFlag isn’t set. Yes you get a tiny little bit more overhead, but you gain so much flexibility, modularity and readability.

For the state machine, which calls the state I also use a switch case, but that one is also macro’lized.

#undef State

#define State(x) break; case x: if(x##F())
extern bit newWals(void) {
	if(enabled) switch(state){
		default: case newWalsIDLE: return true;

		State(powerOff) {
			nextState(startPosition, 50); } // 5 second delay

		State(startPosition) {
			nextState(checkSensors, 8); }

		State(checkSensors) {
			if(errorCode) 	nextState(ALARM, 0);
			else 			nextState(pushProfile, 30); } // 3 second time to start motor
...
...

Here ‘State(x)’ is defined differently. It is a case label x which calls xF(); And it monitors with an if-statement if this state function returns ‘true’ = state is finished.

The function nextState sets the state variable and sets a delayed execution time, in the event you need such a time.

We often have to set a motor using a CAN bus transmission or set a pneumatic cylinder and wait untill it is in position. And because things can get stuck we often use the same timer to handle a timeOut.

I programmed a machine with loading arms. To home these arms, I first need to send a can message to a slave to move the arms to home positions. I poll for a respons so I know the arms are in positions. And lastly I set a pneumatic cilinder to swap them outside the machine. I can do all of this in one state.

State(homeArm) {
	entryState{
		moveMotor(homePosition); // sends can message to move motor
	}
	onState{
		if(motorState & IN_POSITION) { // flag is set by can bus routine
			exit = true; 
		} 
	}
	exitState{
		loadArmInside = 0; // swing arm outside
		return true; 
	}
}

And in the state machine (switch-case)

...
State(homeArm) {
	nextState(arm2pickUpHeight, 250);  // 2.5 second delay to let arm swing outside
} 
...

The next state function handles the hidden ‘runOnce’ flag for the entry state, the exitFlag is cleared, the next state is selected and if the timer is set other than 0, I clear the enabled flag (prevents SM from being executed) and I set the timer. (when enabled is false and the timer reaches 0, enabled is set at true, and the SM will run the next state)

The very same timer used for a timeout function:

State(swingArmInside) {
	entryState{
		loadArmInside = 1; // swing arm inside
		armT = 100;		   // 1 second time out
	}
	onState{
		if(armIsInSensor) {
			exit = true; 
		} 
		if(!armT) {		// if timer reaches 0, set error
			errorReason = ARM_IS_STUCK;
			exit = true;
		}
	}
	exitState{
		// nothing needed here
		return true; 
	}
}

And in the state machine:

...
State(swingArmInside) {
	if(errorReason) nextState(ALARM,0);
	else            nextState(armToBase,0); // we know with sensor that arm is in, so no delay needed
}
...

I use the errorReason to jump to the ALARM state if the reason != 0. Otherwise I resume the normal routine.

I find this the most elegant code I have ever written. I especially like my macros of which everybody seems to be allergic. The state machines are generated for me using a simple state diagram.
The software is documentated before the SW is written.
The structure for which code belongs where, is already in place.
A decrementing SW timer for time-outs, in-state delays and between-state delays are already in place.

The timer SW is also generated, I only have to say what the timer base is. 1ms, 10ms or 100ms (1s not used). So I dont need an unsigned long for every timer function.

And what I also thought of. If a state of the state diagram has no outgoing arrow. The state will return automatically to IDLE. IDLE state lets the state machine itself return true. This I use for nested state machine.

Our machine work with bike wheels, so often I have a nested SM for rolling a wheel in or out, such a state looks like:

State(rollWheelIn) {
	entryState{
		handleWheelSetState(rollIn);	// sets the state at rolling in a wheel
	}
	onState{
		if(handleWheel()) { // function call to nested state machine, returns true when wheel in inside the machine
			exitFlag = true;
		}
	}
	exitState{
		return true;
	}
}

State(rollWheelOut) {
	entryState{
		handleWheelSetState(rollOut);	// sets the state at rolling out a wheel
	}
	onState{
		if(handleWheel) { // function call to nested state machine, returns true when wheel in outside the machine
			exitFlag = true;
		}
	}
	exitState{
		return true; 
	}
}

Just 2 lines of code in a generated piece of code to handle a nested state machine. I use the same state machine for both actions… because I can. I also use the very same timer of the “parent” state machine because I can.

Somewhere in the nested state machine’s header file, this line exists:
#define handleWheelT mainStateMachineT // or whatever name.

This has to be set manually. An #error directive will pop an error if you forget this, telling you precisely what to do.

bask185: This is the most horrific abomination in coding I have ever seen.

If that's true then get around more and then get over it. Harden yourself, there's much, much worse out there and it's not all goto-code.

You may like those macros you're used to but they don't make your code easier for me to read, just the opposite.

Do you have problems with enumerators in state machines? There can be one on Arduino since RAM is limited and enums take up RAM even when forced to use 8-bit values.

But mostly this is a hobby forum and getting beginners to work with state machines at all is kind of a goal. Getting them to see structure apart from code usually takes longer.

GoForSmoke:
This is an example made to show how to remove delays (un-delay) from simple code using a finite state machine packaged into a function with a 1-shot millis timer. There is one timer for several wait-untils.

// add-a-sketch_un-delay 2018 by GoForSmoke @ Arduino.cc Forum

// Free for use, Apr 30/18 by GFS. Compiled on Arduino IDE 1.6.9.
// This sketch shows a general method to get rid of delays in code.
// You could upgrade code with delays to work with add-a-sketch.

#include <avr/io.h>
#include “Arduino.h”

const byte ledPin = 13;
unsigned long delayStart, delayWait;

void setup()
{
  Serial.begin( 115200 );
  Serial.println( F( “\n\n\n  Un-Delay Example, free by GoForSmoke\n” ));
  Serial.println( F( “This sketch shows how to get rid of delays in code.\n” ));

pinMode( ledPin, OUTPUT );
};

/* The section of the original sketch with delays:
*

  • digitalWrite( ledPin, HIGH );  –  0
  • delay( 500 );
  • digitalWrite( ledPin, LOW );    –  1
  • delay( 250 );
  • digitalWrite( ledPin, HIGH );  –  2
  • delay( 250 );
  • digitalWrite( ledPin, LOW );    –  3
  • delay( 250 );
  • digitalWrite( ledPin, HIGH );  –  4
  • delay( 1000 );
  • digitalWrite( ledPin, LOW );    –  5
  • delay( 1000 );
    */

byte blinkStep; // state tracking for BlinkPattern() below

void BlinkPattern()
{
  // This one-shot timer replaces every delay() removed in one spot. 
  // start of one-shot timer
  if ( delayWait > 0 ) // one-shot timer only runs when set
  {
    if ( millis() - delayStart < delayWait )
    {
      return; // instead of blocking, the undelayed function returns
    }
    else
    {
      delayWait = 0; // time’s up! turn off the timer and run the blinkStep case
    }
  }
  // end of one-shot timer

// here each case has a timed wait but cases could change Step on pin or serial events.
  switch( blinkStep )  // runs the case numbered in blinkStep
  {
    case 0 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 0 doing something unspecified here at " ));
    Serial.println( delayStart = millis()); // able to set a var to a value I pass to function
    delayWait = 500; // for the next half second, this function will return on entry.
    blinkStep = 1;  // when the switch-case runs again it will be case 1 that runs
    break; // exit switch-case

case 1 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 1 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 2;
    break;

case 2 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 2 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 3;
    break;

case 3 :
    digitalWrite( ledPin, LOW );
    Serial.println( F( "Case 3 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 250;
    blinkStep = 4;
    break;

case 4 :
    digitalWrite( ledPin, HIGH );
    Serial.println( F( "Case 4 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 5;
    break;

case 5 :
    digitalWrite( ledPin, LOW );
    Serial.print( F( "Case 5 doing something unspecified here at " ));
    Serial.println( delayStart = millis());
    delayWait = 1000;
    blinkStep = 0;
    break;
  }
}

void loop()  // runs over and over, see how often
{           
  BlinkPattern();
}




I have no problems with nesting FSM's or running them in parallel in non-blocking code.

Get rid of delay() and execution blocking code and generally your code will run many times faster than blocky code. A delay(1) wastes 16000 cycles when small tasks might run < 100 to < 400 cycles. I have posted examples where a void loop() counter showed 66700 loops per second unless the line

// delay(1);

is uncommented and then loop() runs 980 times a second.

Learn to write non-blocking code using time values and FSM's is good. Make it as loop()-driven as possible, you can add loads of code to it.

This method is my favorite one by far- thank you so much. It has simplified the problem greatly for me since it looks so much like using the delay function.
I’ll be looking at what bask185 wrote in details and also try it, but for now this method will do perfectly.
Thank you all again !

Thank you about the simple part as that is one of the main things I want in my examples.

I do have examples with some utilities (mainly buttons, blinkys and loop counter) that can blend in with any others in the set. If you want any, sing out or send me a PM.

GoForSmoke:
If that’s true then get around more and then get over it. Harden yourself, there’s much, much worse out there and it’s not all goto-code.

In my defence this example was in assembly… have you any idea what it takes to simply ‘add’ a state?

GoForSmoke:
You may like those macros you’re used to but they don’t make your code easier for me to read, just the opposite.

I believe this is just the macro allergy talking. I get this so often. People also always complain about my identation style (python style). People take one simple glance, come to the conclusion that it is non-standard and from that point they automatically complain with standard phrases: “your code is unreadable”, "it is a visual mess’’, “please format your code properly so others can read it”. I mean they never gave it a even a small chance. But anyways I did not use it here.

And with macro;s… well I already said macro allergy: “Ain’t no good programmer using no macro’s no more.” or “using non-standard code makes your code harder to read” ← this is just a standard phrase and not a proven fact, yet…

But for real now, if you can code you cannot not understand

State(rollWheelIn) {
 entryState{

 }
 onState {
 
    exitFlag = true;
 }
 exitState {
    return true;
 }
}

it litterly says what block is what. Given the information that you are looking at a state of a state machine and know just a little bit about FSMs you should have 0.0 trouble figuring it out. The macro’s are also to be found on top of the states. Besides that they are designed with the idea that it is not needed to know how they precisely look like.

When I type it out it looks like:

static bit powerOffF(void) {
 if(runOnce) {

 }
 runOnce = false; 
 if(!runOnce) {

        exit = true; 
        }
 if(!exit) return false; 
        else {
 
 return true; 
        }  
}

For me this just adds redundant information. It would be present in every state and fill up lines. I chose to display that was is relevant with well-syntaxed macro’s. Less code means less code to brick :slight_smile: and how more code you can generate how less typo’s you get to make.

GoForSmoke:
Do you have problems with enumerators in state machines?

Not ever really I use enums a lot, but were code space to become a problem I would simply swap them for #defines. With VS code I can multi-edit lines and I can enumerate numbers with a few button presses. Or I’d modify my scripts to make and enumerate the #defines for me.

GoForSmoke:
But mostly this is a hobby forum and getting beginners to work with state machines at all is kind of a goal. Getting them to see structure apart from code usually takes longer.

The scripts I am building are able to set up a working compilable project folder which only needs filling in. When it is ready I will post it on this forum and it will come with an example in which I write SW for an alarm clock from scratch using the scripts.

bask185: In my defence this example was in assembly.... have you any idea what it takes to simply 'add' a state?

If I wrote what was to be added to and didn't use enums then it could mean the PITA of renumbering. I've dealt with worse. Back in the 1980 I even wrote worse while transitioning from a TI-59 to a PC with Basic.

I believe this is just the macro allergy talking.

That's your choice. I use few macros ever for -reasons- that I shared. If I want to go memorizing code, I'll fill in some of the C++ memory holes I acquired in 2000.

From what I've read, #define has overhead in Arduino.

bask185: For work I had to learn assembly and they do precisely this. This is the most horrific abomination in coding I have ever seen. Followd by all the 'inc state' or state++; Also terror.

It's not immediately obvious to me whether the long Reply #9 is a new question or an attempt to help the OP.

...R

@laet9, I hope all the answers are not confusing. They do the same thing.

Waiting with millis() with an extra variable or avoiding to enter the FSM while waiting or having a wait-state, that’s all okay.

A state has often code when entering that state and code when leaving that state. So the “entryState”, “onState” and “exitState” solution by bask185 is very nice, but it requires an extra variable. Some don’t like that extra variable and how it is used.

I don’t mind having a lot of states. My solution uses three states for a single thing. I often have to add an extra “INIT” state to set things right before entering the real state. The well-known example by Majenko (The Finite State Machine | Majenko Technologies) calls them “Transitional State”.

You can just use what seems best for you. What you think is the most simple and most beautiful solution is the best for you.

Koepel: @laet9, I hope all the answers are not confusing. They do the same thing.

Waiting with millis() with an extra variable or avoiding to enter the FSM while waiting or having a wait-state, that's all okay.

A state has often code when entering that state and code when leaving that state. So the "entryState", "onState" and "exitState" solution by bask185 is very nice, but it requires an extra variable. Some don't like that extra variable and how it is used.

I don't mind having a lot of states. My solution uses three states for a single thing. I often have to add an extra "INIT" state to set things right before entering the real state. The well-known example by Majenko (https://majenko.co.uk/blog/finite-state-machine) calls them "Transitional State".

You can just use what seems best for you. What you think is the most simple and most beautiful solution is the best for you.

Don’t worry, most of what has been said was clear ! It’s just that it’s my first « big » project with Arduino and as a beginner I realized, maybe a little late, that delay() was probably not the best option. I was trying to do it the easiest way, with as few states as possible, but I guess I’m now realizing -thanks to all of you- that more states don’t equal to a more complicated code, it’s the opposite actually. I’ll certainly take a look at the example ! In the end, I’ve decided to explore with some of the methods, and they all seem to be doing exactly what I wanted. :) I’ll just end up keeping the version that’s easier to understand for someone else reading my code.