How to perform a non blocking case delay in a Finite State Machine.

Good morning everyone :slight_smile:

I managed to write my first simple FSM with the help of a tutorial from the web and its working quite good so far.
But one thing I try to programm is a pause for one case that is not blocking the entire machine. I tryed several millis in different positions of my code, but non seems working for me.

I drew a statechart so you can see what I try to manage and hopefully its correct and you can watch it:

Right now, the delay between noLight and the other cases is blocking my FSM even with using millis.

My code:

#include <FastLED.h>

#define NUM_LEDS 16

CRGB ledStrip [NUM_LEDS];

const int buttonPin = 4;
const int ledStripPin = 8;
const int caseDelay = 1000;

unsigned long signalMillis = 0;
unsigned long signalInterval = 0;

enum statusType {whiteLight, yellowLight, noLight};

statusType currentStatus = whiteLight;

void setup() {

  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledStrip, OUTPUT);

  FastLED.addLeds<NEOPIXEL, 8>(ledStrip, NUM_LEDS);
  FastLED.show();

}

void loop() {

  if (millis() - signalMillis >= signalInterval) {

    switch (currentStatus) {

      case whiteLight:
        currentStatus = whiteLightFunction();
        break;

      case yellowLight:
        currentStatus = yellowLightFunction();
        break;

      case noLight:
        currentStatus = noLightFunction();
        signalMillis = millis();
        signalInterval = caseDelay;
        break;
    }
  }
}

statusType whiteLightFunction() {

  if (digitalRead(buttonPin) == LOW) {

    return (yellowLight);

  } else {

    fill_solid(ledStrip, NUM_LEDS, CRGB::White);
    FastLED.setBrightness(100);
    FastLED.show();

    return (whiteLight);
  }
}

statusType yellowLightFunction() {

  if (digitalRead(buttonPin) == LOW) {

    fill_solid(ledStrip, NUM_LEDS, CRGB::Yellow);
    FastLED.setBrightness(100);
    FastLED.show();

    return (yellowLight);

  } else {

    return (noLight);
  }
}

statusType noLightFunction() {

  fill_solid(ledStrip, NUM_LEDS, CRGB::Black);
  FastLED.show();


  if (digitalRead(buttonPin) == LOW) {

    return (yellowLight);

  } else {

    return (whiteLight);
  }
}

The button is connected to ground, thats why its LOW if pushed.

Do I have to put some buttonstates to get passed that problem, like LOW: 1 or 0, and HIGH: 1 or 0? Or did I wrote just a simple delay, but it looks like a millis?

Thanks for helping me with my problem to understand why even millis is now blocking my program.

If you want the program to stay in a state for a period but not block the program, then save the millis() value before changing to the state then in the state check whether the current value of millis() - the start value is greater than the required period and if so, take action

This is a very common requirement - a state that has a time-out associated with it. You can use a single
time variable for all states, and always set it when changing state. States that have a time-out transition then
just need to do the comparison. To facilitate this use some helper functions:

int state = INIT_STATE ;
unsigned long last_state_change = 0L ;

void change_state (int new_state)
{
  if (new_state != state)
  {
    last_state_change = millis() ;   // or maybe micros()
    state = new_state ;
  }
}

bool state_timed_out (unsigned long timeout)
{
  return millis() - last_state_change >= timeout ;  // or maybe micros()
}

Then you can have code like

  case FOO_STATE:
    if (state_timed_out (100))
      change_state (BAR_STATE) ;
    if (digitalRead (pin) == LOW)
      change_state (BAZ_STATE) ;
    break ;

Hey UKHeliBob,
I tried some millis in several positions, but never got to delay the noLight function. I only delayed the yellowLight function, but even that is not quite right.
It seems like the time counts in the background and after the button has been pressed the yellow Light goes on and if I press it again maybe around after 750ms, the yellow light stays on only for 250ms left if millis is set to 1000. Its not reseting the time after the button has pressed again.

Hey MarkT
Thanks for posting some code. I try to combine it with my program and will see if it works.
I havent even thought about to put a button == LOW in the loop inside the different cases, sounds like that would work.

I will post if something works or isn't working :slight_smile:

Thanks for both of you.

Maybe I don't understand your code but it appears to me that you do not reset your clock for the next test unless the light is off. (case noLight:)

I think:

Your white light will stay on in the idle state but it will retest continuously.
Your yellow light will light immediately when you press the button because you are continuously retesting.
Your yellow light will stay on as long as you hold the button since the test interval trigger will stay true.
Your yellow light will immediately turn off when the button is released and the light will stay off until new next test interval is reached.

If you only want lights to change when synchronized to the test interval you are working in the right direction.
If you want lights to change in time relative to when the button is pressed, you need to test the button inside the main loop.

edit - I see that is just about what you originally wanted after re-reading your diagram. However you will not catch a second button press during the black period because your loop is waiting for the next test interval before looking at the button state again.

A simple example - State Machine Tutorial

There are several others on the forum, as well.

Hey guys,

I found a way of staying black for some time without blocking the machine with a 4th state called noLightPause().

Now if I let go the button it stays black and after the time has passed it goes to white. And If I press the button and hold it during the pause it goes back to yellow. Almost what I am trying to achieve :D.

One last thing is, if it is possible to press the button during the noLightPause() case to go back to the yellowLight() case directly and not wait till the time has passed, but just goes to whiteLight() if the time has passed normaly.

Here is my code

#include <FastLED.h>

#define NUM_LEDS 16

CRGB ledStrip [NUM_LEDS];

const int buttonPin = 4;
const int ledStripPin = 8;
const int ledOnBoard = 13;
const int interval = 100;

int ledOnBoardState = LOW;

unsigned long previousMillis = 0;
static unsigned long ts;

enum statusType {whiteLight, yellowLight, noLightPause, noLight};

statusType currentStatus = whiteLight;

void setup() {

  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledStrip, OUTPUT);
  pinMode(ledOnBoard, OUTPUT);

  FastLED.addLeds<NEOPIXEL, 8>(ledStrip, NUM_LEDS).setCorrection(DirectSunlight);
  FastLED.show();
}

void loop() {

  //----------------------------------------
  // just to see if the machine is blocking something

  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    if (ledOnBoardState == LOW) {
      ledOnBoardState = HIGH;
    } else {
      ledOnBoardState = LOW;
    }
    digitalWrite(ledOnBoard, ledOnBoardState);
  }

  //----------------------------------------

  switch (currentStatus) {

    case whiteLight:
      currentStatus = whiteLightFunction();
      break;

    case yellowLight:
      currentStatus = yellowLightFunction();
      break;

    case noLightPause:
      ts = millis();
      currentStatus = noLightPauseFunction();
      break;

    case noLight:
      if (millis() - ts > 1000) {
        currentStatus = noLightFunction();
      
      break;
      }
  }
}

statusType whiteLightFunction() {

  if (digitalRead(buttonPin) == LOW) {

    return (yellowLight);

  } else {

    fill_solid(ledStrip, NUM_LEDS, CRGB::White);
    FastLED.setBrightness(100);
    FastLED.show();

    return (whiteLight);
  }
}

statusType yellowLightFunction() {

  if (digitalRead(buttonPin) == LOW) {

    fill_solid(ledStrip, NUM_LEDS, CRGB::Yellow);
    FastLED.setBrightness(100);
    FastLED.show();

    return (yellowLight);

  } else {

    return (noLightPause);
  }
}

statusType noLightPauseFunction() {

  fill_solid(ledStrip, NUM_LEDS, CRGB::Black);
  FastLED.show();

  if (digitalRead(buttonPin) == LOW) {

    return (yellowLight);

  } else {

    if (digitalRead(buttonPin) == HIGH) {

      return (noLight);
    }
  }
}

statusType noLightFunction() {

  if (digitalRead(buttonPin) == LOW) {

    return (yellowLight);

  } else {

    return (whiteLight);
  }
}

If we could figure that last part out, that would be amazing :).

Thanks for you help so far.

One last thing is, if it is possible to press the button during the noLightPause() case to go back to the yellowLight() case directly and not wait till the time has passed, but just goes to whiteLight() if the time has passed normaly.

Sure. Read the button state and if it becomes pressed then change the state instead of waiting from the period to end. There can be more than one way to exit a state

I think you should remove the button press action from each state. Each state sets current output and sets the next state and a button press changes the current state if appropriate and sets the next state.

The main loop executes the current state at each interval trigger and a button press makes a state change if appropriate and executes the new current state immediately while updating the next state if appropriate.

If the button is pressed during the white state, remain white and next interval is yellow.
If the button is pressed during the yellow state, remain yellow and next interval is black.
If the button is pressed during the black state, change the state to yellow? and the next interval is white?
or
If the button is pressed during the black state, change the state to yellow? and the next interval is black?
or
If the button is pressed during the black state, remain black? and next interval is yellow?