Efficient way to call redundant conditions and tasks within state machine?

Hi everyone! I'm sorry if the title is confusing-- I wasn't really sure how to word my question in one sentence but hopefully it's understandable. I'm looking to build a state machine, implementing millis() for timing. I have 8 conditions the program is looking for in the default state. If any of those conditions occur, the same millis() code will first run for 5000ms regardless of the condition that was triggered, and immediately following that same millis() code would be a function/state dependent on the condition that was initially triggered in the default state, while simultaneously also returning to looking for any of the 8 conditions in the default state.

The flow of the states would look like this, as an example:

Default state -> IF (A0 > 2.0 volts) -> Warning ->Troublecode1 (also check for conditions in default state) -> Default state (break);
Default state -> IF (A1 < 1.0 volts) -> Warning ->Troublecode2 (also check for conditions in default state) -> Default state (break);
Default state -> IF (A2 >= 3.0 volts) -> Warning ->Troublecode3 (also check for conditions in default state) -> Default state (break);
Default state -> IF (A3 = 1.5 volts) -> Warning ->Troublecode4 (also check for conditions in default state) -> Default state (break);
Default state -> IF (A4 = 2.0 volts) -> Warning ->Troublecode5 (also check for conditions in default state) -> Default state (break);

The above only shows 5 conditions, but you get the idea I'm sure. The same "Warning" state, implementing millis(), will always be ran before going to the specific Troublecode state (specific to the condition that took place).

I suppose the best way to solve this (please correct me if I'm wrong) is to actually embed the millis() function of the "warning" code to actually be in each Troublecode state, and just call the specific Troublecode state instead of going to a separate Warning state first, so like this:

Default state -> IF (A0 > 2.0 volts) -> Troublecode1 (also check for conditions in default state) -> Default state (break);
Default state -> IF (A1 < 1.0 volts) -> Troublecode2 (also check for conditions in default state) -> Default state (break);

The problem that I can't seem to find a resolution for after searching several forums and Google, in general, is how to call what's needed for the "Warning" portion of the code using millis(), for each Troublecode state, without writing out the millis() code completely over again for each Troublecode state, since the millis() code for the Warning will always be the same.

Is there a way to simplify the calling of the same millis() code for Warning, for each of the Troublecode states, without writing the same block of code over again for each Troublecode state?? How can I also simplify the checking of the same conditions that are in the default state, while each Troublecode state is running, without writing the conditions all over for each Troublecode state? Obviously the answer to these would be to have a separate function written out for the conditions, a separate function for the Warning, a separate function for each Troublecode state, and just call the functions below, IF you were able run multiple functions at the same time:

Default state -> Conditions() -> IF (A0 > 2.0 volts) -> Warning() -> Troublecode1() & Conditions() -> Default state (break);
Default state -> Conditions() -> IF (A1 < 1.0 volts) -> Warning() -> Troublecode2() & Conditions() -> Default state (break);

Obviously, we know that multiple functions can't be called to run at the same time, so I'm not sure how to check for the same list of conditions as from the start of the loop() again while running a Troublecode function simultaneously, without re-writing the conditions to return to Warning for every instance of a Troublecode state.

Also, is there a more efficient way laying this code out if I want a specific millis() code and a specific message written on an OLED display for each Troublecode state, after the same millis() code for the Warning has been ran for the 5000 ms?

My apologies for not having some true example code of my millis() programming. I can write some up if necessary, I just haven't gotten that far yet because I'm stuck with trying to figure out how to lay out this state machine. If anyone is willing to give some advice on this I'd be very appreciative!! Thank you so much!

-Andrew

Your Warning states need 2 parameters. The first being the period to output the warning and the second the state to move to when the warning period ends. If all of the warning periods are the same then you only need to set the target state. Set them/it before moving to the warning state. That way you can have just 1 Warning state and use it for different purposes.

Does the trouble code display for a set time and then ends or, is a given code displayed continuously until a new code or a reset comes in?

Rephrasing UKHeliBob's idea, if you detect an error while in the default state, set a variable to indicate the error type and switch to the warning state. When the warning state is finished, switch to the correct troublecode state based on the variable.

You can also set a variable in the default state to indicate the required delay before switching to the warning state and use that in the warning state.

Big thanks to the 3 of you for you replies! I value your guys' input and time!

UKHeliBob:
Your Warning states need 2 parameters. The first being the period to output the warning and the second the state to move to when the warning period ends. If all of the warning periods are the same then you only need to set the target state. Set them/it before moving to the warning state. That way you can have just 1 Warning state and use it for different purposes.

sterretje:
Rephrasing UKHeliBob's idea, if you detect an error while in the default state, set a variable to indicate the error type and switch to the warning state. When the warning state is finished, switch to the correct troublecode state based on the variable.

You can also set a variable in the default state to indicate the required delay before switching to the warning state and use that in the warning state.

Thanks for this suggestion. I've read over both of your replies a few times, and it's certainly due to my ignorance, but I don't seem to grasp exactly how to accomplish the part about setting the variable, and later calling the variable. It may be due to the fact that I left something important out of my initial post that I didn't think would matter-- the Troublecode states will just be flashing an LED a certain number of times, at a 1000 ms duty cycle, pause, and repeat the flash(es) one more time, then return to the default state. So Troublecode3 will flash an LED 3 times, pause, flash the LED 3 more times, then break. It will require the use of millis() of course. Sorry that I failed to mention that initially... does that change things? If not, is there a possibility of drafting a really quick, simple example of what you guys mean?? Thanks again!

dougp:
Does the trouble code display for a set time and then ends or, is a given code displayed continuously until a new code or a reset comes in?

Thanks for the response Doug. The trouble code state will flash an LED a certain number of times, based on the condition that was met, and I'm thinking I'll write the program so that each Troublecode state is allotted 30 seconds to run, before exiting the specific Troublecode state and returning to the default state by itself.

This is the sort of thing that I had in mind

state = 0
start of loop()

  switch(state)
    case 0    //read sensors
      IF (A0 > 2.0 volts)
        state = 1
      else IF (A1 < 1.0 volts) 
        state = 2
      else IF (A2 >= 3.0 volts)
      `state = 2
      //etc
      end if
    break
      
    cases 1, 2, 3 etc //warning needed
      warning code goes here
      set count for trouble code based on state.  Maybe from an array indexed on state
      state = 99
    break

    case 99    //output trouble code flashes
      code to flash LED using millis() and count
      when complete set state to zero
    break
     
end of loop()

Start with the framework

// possible states of state machine
#define ST_DEFAULT 0
#define ST_WARNING 1
#define ST_TROUBLE1 11
#define ST_TROUBLE2 12
// current state; to default state
byte currentState = ST_DEFAULT;

// possible error codes; not that those must be multiples of 2
#define ERR_NOERROR 0
#define ERR_A0_HIGH 1
#define ERR_A1_LOW 2
// error flags 'register'; set to no error
int errorFlags = ERR_NOERROR;

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop()
{
  switch(currentState)
  {
    case ST_DEFAULT:
      currentState = defaultState();
      break;
    case ST_WARNING:
      currentState = warningState();
      break;
    case ST_TROUBLE1:
      currentState = trouble1State();
      break;
    case ST_TROUBLE2:
      currentState = trouble2State();
      break;
    default:
      Serial.println("Unknown state");
      // stop forever
      for (;;);
      break;
  }
}

The errorFlags 'register' uses bits for the error conditions; bit 0 is used for trouble 1, bit 1 for trouble 2 etc. Therefore the error codes need to be multiples of 2.
This allows you to catch multiple errors.

Both currentState and errorFlags are global variables so we can use them in all our functions.

And add the framework of the functions that will be executed in the specific states; those function will return the next state.

/*
  code for default state; checks the analog voltages
  Returns:
    ST_WARNING if there was an error, else ST_DEFAULT
*/
byte defaultState()
{
}

/*
  code for warning state
  Returns:
    next state
  Note:
    the state of the built-in led indicates if the code starts or continues
*/
byte warningState()
{
}

/*
  code for trouble 1 state
  Returns:
    next state (can be current state)
*/
byte trouble1State()
{
}

/*
  code for trouble 2 state
  Returns:
    next state (can be current state)
*/
byte trouble2State()
{
}

Now we can fill in the functions.

defaultState() will read the analog inputs and places possible errors in the errorFlags 'register'. If there are no errors, it will return ST_DEFAULT so the code will stay in the current state, else it will return ST_WARNING and the next iteration of loop() will execute the ST_WARNING state.

I have simulated the analog voltages with a call to random().

/*
  code for default state; checks the analog voltages
  Returns:
    ST_WARNING if there was an error, else ST_DEFAULT
*/
byte defaultState()
{
  // clear the errorFlags register
  errorFlags = ERR_NOERROR;
  //if (analogRead(A0) > 500)
  if (random(0, 1023) > 500)
  {
    errorFlags |= ERR_A0_HIGH;
  }
  //if (analogRead(A1) < 200)
  if (random(0, 1023) > 500)
  {
    errorFlags |= ERR_A1_LOW;
  }

  if (errorFlags != ERR_NOERROR)
  {
    // switch to warning state if there are errors
    return ST_WARNING;
  }
  else
  {
    // else stay in the default state
    return ST_DEFAULT;
  }
}

Next the warningState(); in this example it will switch on the built-in led for 3 seconds and next return the nexxt state based on the error code.

/*
  code for warning state
  Returns:
    next state; ST_WARNING while 'delay' is in progress
  Note:
    the state of the built-in led indicates if the code starts or continues
*/
byte warningState()
{
  static unsigned long startTime;

  // switch the built-in led on
  if (digitalRead(LED_BUILTIN) == LOW)
  {
    digitalWrite(LED_BUILTIN, HIGH);
    startTime = millis();
    Serial.println("Warning state entered");
  }

  // if warning delay has lapsed
  if (millis() - startTime > 3000)
  {
    Serial.println("Warning state finished");
    // switch the built-in led off
    digitalWrite(LED_BUILTIN, LOW);

    // determine next state and return it
    return getNextState();
  }

  // stay in warning state
  return ST_WARNING;
}

Note: the code uses the status of the built-in led as a flag; you cn use a dedicated static variable for this as well if needed.

The code uses an additional function that determines the next state based on the error flags.
Place the below at the end of the code.

/*
  get the next state depending on the error code
  Returns:
    next state depending on error code; default state if no errors remaining
*/
byte getNextState()
{
  // return the next state based on the error code
  if ((errorFlags & ERR_A0_HIGH) == ERR_A0_HIGH)
  {
    return ST_TROUBLE1;
  }
  if ((errorFlags & ERR_A1_LOW) == ERR_A1_LOW)
  {
    return ST_TROUBLE2;
  }

  return ST_DEFAULT;
}

You can prioritise the error handling by moving the ifs around.

Next you can implement the functions for the trouble1 state and trouble2 state.

/*
  code for trouble 1 state
  Returns:
    next state (can be current state)
*/
byte trouble1State()
{
  // variable to indicate if we enter first time or not
  static bool inProgress = false;
  
  if (inProgress == false)
  {
    // set the inProgress flag; next time that troubleState1 is called, this block will be skipped
    inProgress = true;
    Serial.println("Trouble1 state entered");
    // indicate that we need to stay in trouble1 state
    return currentState;
  }

  delay(2000); // little lazy
  // clear the inProgress flag
  inProgress = false;

  // clear the specific error flag
  Serial.print("Flags (before) = "); Serial.println(errorFlags, HEX);
  errorFlags &= (ERR_A0_HIGH ^ 0xFFFF);
  Serial.print("Flags (after) = "); Serial.println(errorFlags, HEX);
  Serial.println("Trouble1 state finished");

  // return the next state depending on the remaining error code
  return getNextState();
}
byte trouble2State()
{
  // variable to indicate if we enter first time or not
  static bool inProgress = false;
  if (inProgress == false)
  {
    // set the inProgress flag; next time that troubleState2 is called, this block will be skipped
    inProgress = true;
    Serial.println("Trouble2 state entered");
    // indicate that we need to stay in trouble1 state
    return currentState;
  }

  delay(500); // little lazy
  // clear the inProgress flag
  inProgress = false;

  // clear the specific error flag
  Serial.print("Flags (before) = "); Serial.println(errorFlags, HEX);
  errorFlags &= (ERR_A1_LOW ^ 0xFFFF);
  Serial.print("Flags (after) = "); Serial.println(errorFlags, HEX);
  Serial.println("Trouble2 state finished");

  // return the next state depending on the remaining error code
  return getNextState();
}

Notes:
if your (troubleX) functions need timed code (e.g. flashing led), use the same approach as in the warningState() using the line static unsigned long startTime; .

The output looks like

Flags = 2
Warning state entered
Warning state finished
Trouble2 state entered
Flags (before) = 2
Flags (after) = 0
Trouble2 state finished

Flags = 2
Warning state entered
Warning state finished
Trouble2 state entered
Flags (before) = 2
Flags (after) = 0
Trouble2 state finished

Flags = 2
Warning state entered
Warning state finished
Trouble2 state entered
Flags (before) = 2
Flags (after) = 0
Trouble2 state finished

Flags = 3
Warning state entered
Warning state finished
Trouble1 state entered
Flags (before) = 3
Flags (after) = 2
Trouble1 state finished
Trouble2 state entered
Flags (before) = 2
Flags (after) = 0
Trouble2 state finished

Flags = 1
Warning state entered
Warning state finished
Trouble1 state entered
Flags (before) = 1
Flags (after) = 0
Trouble1 state finished

Flags = 2
Warning state entered

Empty lines added for clarity.

Do yourself a favour and keep the comments above the functions in place; it's a good habit to have documentation as to what a function does.

1 Like