State Machine Remember Previous State

I am writing code for a peristaltic doser for my aquarium. The doser is controlled via an interface on my phone. The doser will have the option of dosing from 1 / day to 48 / day. I have decided to use a state machine to achieve this. My question is if I am in say the 48 times a day dose and it is time to dose the state machine should exist the current state and go to the dose state. The dose state will be the same for all the programming states. Once the dosing function is complete is there a way to remember what previous state it came from (in the example above it would be the 48 times a day?

State Machine SetUp

enum D1_MainFunction {D1_MAINFUNCTIONIDLE, D1_BOOSTMODE, D1_1DOSEDAY, D1_2DOSEDAY, D1_6DOSEDAY, D1_12DOSEDAY, D1_24DOSEDAY, D1_48DOSEDAY, D1_DOSERACTIVE};
D1_MainFunction D1MainFunctionState = D1_MAINFUNCTIONIDLE;

int D1_PREVSTATE;   //???????WILL THIS REMEMBER THE PREVIOUS STATE

State Machine Function

void D1_MainFunction()
{
  EEPROM.get(10, D1_DoseTime);
  EEPROM.get(0, D1_DailyDosageFloat);
  switch (D1MainFunctionState)
  {
    case D1_MAINFUNCTIONIDLE:
    {
      if (D1_CalibrateActive)
      {
        digitalWrite(DOS1, HIGH);
      }
      else if (D1_BoostActive)
      {
        digitalWrite(DOS1, HIGH);
      }
      else
      {
        digitalWrite(DOS1, LOW);
      }
      break;
    }
    if (D1_SolutionFlag)
    {
      case D1_BOOSTMODE:
      {
        if (D1_ALKADoseActive)
        {
          D1_ALKATecFlag = true;
          D1_BoostActive = true;
        }
        break;
      }
      case D1_1DOSEDAY:
      {
        D1_Time = (D1_DailyDosageFloat * D1_DoseTime) + Round;
        if (TimeNow % Mod1 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(1, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }
      case D1_2DOSEDAY:
      {
        D1_Time = ((D1_DailyDosageFloat / 2) * D1_DoseTime) + Round;
        if (TimeNow % Mod2 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(2, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }
      case D1_6DOSEDAY:
      {
        D1_Time = ((D1_DailyDosageFloat / 6) * D1_DoseTime) + Round;
        if (TimeNow % Mod6 == 0 )
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(6, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }
      case D1_12DOSEDAY:
      {
        D1_Time = ((D1_DailyDosageFloat / 12) * D1_DoseTime) + Round;
        if (TimeNow % Mod12 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(12, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }
      case D1_24DOSEDAY:
      {
        D1_Time = ((D1_DailyDosageFloat / 24) * D1_DoseTime) + Round;
        if (TimeNow % Mod24 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(24, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
         }
         break;
      }
      case D1_48DOSEDAY:
      {
        D1_Time = ((D1_DailyDosageFloat / 48) * D1_DoseTime) + Round;
        if (TimeNow % Mod48 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(48, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }
      case D1_DOSERACTIVE:
      {
        //Code to dose
        //HOW TO RETURN TO PREVIOUS STATE TO START NEXT ITERATION????????
        break;
      }
    }
  }
}

Save the timing state to a variable before you switch to the dosing state. Use the previous timing state value to determine the next state to jump to after dosing

1 Like

Ok, and what format is the state? Is it a char? Int?

Yes, if you assign to it

D1_PREVSTATE = D1MainFunctionState;

but why not declare it to be the same type?

D1_MainFunction D1_PREVSTATE;

Should be
D1_MainFunction D1_PREVSTATE; //THIS WILL REMEMBER THE PREVIOUS STATE

Also realize what you're doing - creating a one level call stack. :slight_smile:

It will never be complete. In the "done" state it waits for the next step time to arrive. You only have to calculate the new time between doses when the doses/day is changed.

Oh boys and girls, why do you make the implementation of a simple state machine so complicated :frowning:

there are 2 types of state machines: one where the action is associated with the state, the other where the action is associated with the transitions between states

the other thing to consider is driving the state machine with a stimuli. looks like your code tests various flags and a timer to perform some action and/or switch states

in your 48_dose state, some stimuli should cause a transition from/to the 48 dose state. that transition can administer the dose as well as keep track of the # of doses and possibly generating yet another stimuli the transition from the 48_does state to some other state

when there are many states and stimuli, i prefer a data driven approach using a table (yes, "__()" is a valid function name)

//
// example state machine
//

#include <stdio.h>

#include "stateMach.h"

// ------------------------------------------------
Status
a (void* a)
{
    printf ("%s:\n", __func__);
    return OK;
}

// ------------------------------------------------
Status
b (void* a)
{
    printf ("%s:\n", __func__);
    return OK;
}

// ------------------------------------------------
Status
c (void* a)
{
    printf ("%s:\n", __func__);
    return OK;
}

// ------------------------------------------------
Status
__ (void* a)
{
    printf ("%s:\n", __func__);
    return OK;
}

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

#define N_STATE     3
#define N_STIM      2

typedef enum { S0, S1, S2 } State_t;

typedef Status(*Action_t)(void*) ;

State_t smTransitionTbl [N_STATE] [N_STIM] = {
    {   S1, S2, },
    {   S0, S2, },
    {   S2, S1, },
};


Action_t smActionTbl [N_STATE] [N_STIM] = {
     {  a,  b },
     {  c, __ },
     { __,  a },
};

// ------------------------------------------------
char *msg1 [] = {
    "State machine has 3 states and 2 stimuli.",
    "It has the following state transistion and action routine tables.",
    "A __() represents do nothing and is easily identified in table.",
    "Each row is indexed by a state and each column a stimulus.",
    "Of course, meaningful action routine names are helpful.",
};

char *msg2 [] = {
    "Enter valid stimuli [0-1]:"
};

void
smHelp (void)
{
    for (int i = 0; i < sizeof(msg1)/sizeof(char*); i++)
        printf ("  %s\n", msg1 [i]);

    for (int state = 0; state < N_STATE; state++)  {
        printf ("%8s", "");
        for (int stim = 0; stim < N_STIM; stim++)
            printf (" %d", smTransitionTbl [state][stim]);

        printf ("%8s", "");
        for (int stim = 0; stim < N_STIM; stim++)  {
            if (a == smActionTbl [state][stim])
                printf ("  a()");
            else if (b == smActionTbl [state][stim])
                printf ("  b()");
            else if (c == smActionTbl [state][stim])
                printf ("  c()");
            else
                printf (" __()");
        }

        printf ("\n");
    }

    for (int i = 0; i < sizeof(msg2)/sizeof(char*); i++)
        printf ("  %s\n", msg2 [i]);

}

// ------------------------------------------------
static State_t _smState  = S0;

Status
smEngine (Stim_t stim)
{
    Action_t   func;
    State_t    last = _smState;

    if (N_STIM <= stim)
        return ERROR;

    func        = smActionTbl [_smState] [stim];
    _smState    = smTransitionTbl [_smState] [stim];

    printf ("    stim %d, transition from state %d to %d, exec ",
        stim, last, _smState);

    return (*func) (NULL);
}

Instead of doing this directly:

call a function to force the change to the new state. In that function you not only change the state but also retain the old state in a variable, produce debug output etc. etc.

I don't like snippets, and I don't like to offer code I cannot test.

I do like FSM finite state machines, but not when they do more than a few things, and not when there are flags and other hidden "state" variables being exploited.

However, your dosing states are all very similar.

With this kind of cut, paste and edit code, making changes and corrections becomes a slog.

Therefor, I propose this

// one code to rule them all - set doseDivisor _somewhere_
      case D1_DOSEX:
      {
        D1_Time = (D1_DailyDosageFloat / doseDivisor * D1_DoseTime) + Round;
        if (TimeNow % Mod1 == 0)
        {
          D1_VolFlag = true;
          D1_DoserActiveFlag = true;
          D1_VolUpdateFunction(doseDivisor, 0, false);
          D1MainFunctionState = D1_DOSERACTIVE;
        }
        break;
      }

      case D1_DOSERACTIVE:
      {
        //Code to dose

        D1MainFunctionState = D1_DOSEX;
        break;
      }

HTH

a7

1 Like

i think state machines can make things much more manageable when there are many state/stimuli

i was introduced to them early in my career when i worked on digital data terminals that needed to support a signaling channel

i saw tables indexed by state and stimuli that filled a sheet of paper and which were reviewed for hours. much less time was spent looking at the code. the complexity of the protocol was captured in that table, not the logic of the code

After the Doser state is done I want it to go back to the previous state (48 times a day) for the next time round. This will forwards and backwards continues forever until such time it is changed via my phone to firebase and a callback function.

void DOSETecStateFunction(String path, String value)
{
  if (path == "/D1_Boost")
  {
   if (value == "true")
   {
    D1_BoostActive = true;
   }
   else 
   {
    D1_BoostActive = false;
   }
  }
  else if (path == "/D1_CalibrateButton")
  {
    if (value == "true")
    {
      D1_CalibrateActive = true;
    }
    else
    {
      D1_CalibrateActive = false;
    }
  }
  else if (path == "/D1_ALKATecDose")
  {
    if (value == "true")
    {
      D1_ALKADoseActive = true;
    }
    else
    {
      D1_ALKADoseActive = false;
    }
  }
  else if (path == "/DOSETecActive")
  {
    if (value == "true")
    {
      DOSETecActive = true;
    }
  }
  //PARAMETERS//
  else if (path == "/D1_BoostDosage")
  {
    D1_BoostDosageFloat = value.toFloat();
    EEPROM.put(0, D1_BoostDosageFloat);
    EEPROM.commit();
    Serial.println(D1_BoostDosageFloat);///////////////////
  }
  else if (path == "/D1_CalibrationInput")
  {
    D1_CalibrationInputFloat = value.toFloat();
    D1_DoseTime = 30000/D1_CalibrationInputFloat;
    EEPROM.put(10, D1_DoseTime);
    EEPROM.commit();
    Serial.println(D1_DoseTime);///////////////
  }
  else if (path == "/D1_DailyDosage")
  {
    D1_DailyDosageFloat = value.toFloat();
    EEPROM.put(5, D1_DailyDosageFloat);
    EEPROM.commit();
    Serial.println( D1_DailyDosageFloat);///////////////
  }
  else if (path == "/D1_SolutionInput")
  {
    D1_SolutionInputInt = value.toInt();
    D1_RemainVolume = D1_SolutionInputInt;
    EEPROM.put(15, D1_RemainVolume);
    EEPROM.commit();
    D1_SolutionFlag = true;
    D1_RemainVolFlag = true;
    Serial.println(D1_SolutionInputInt);//////////////////
  }
  //FUNCTIONS//////////////////////////////////////
  else if (path == "/D1_Function")
  {
    if (value == "0")   //OFF/BLEED
    {
      timer.disable(TC);
      D1MainFunctionState = D1_MAINFUNCTIONIDLE;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "1")  //CALIBRATE
    {
      timer.disable(TC);
      D1MainFunctionState = D1_MAINFUNCTIONIDLE;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "2")  //HOSECHANGE
    {
      timer.disable(TC);
      D1MainFunctionState = D1_MAINFUNCTIONIDLE;
      D1_Hose = 0;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.put(20, D1_Hose);
      EEPROM.commit();
      D1_HoseFlagAlert = true;
    }
    else if (value == "3")  //BOOSTMODE
    {
      timer.disable(TC);
      D1MainFunctionState = D1_BOOSTMODE;
      D1_State = 1;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "4")  //1DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_1DOSEDAY;
      D1_State = 2;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "5")  //2DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_2DOSEDAY;
      D1_State = 3;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "6")  //6DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_6DOSEDAY;
      D1_State = 4;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "7")  //12DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_12DOSEDAY;
      D1_State = 5;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "8")  //24DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_24DOSEDAY;
      D1_State = 6;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else                    //48DOSEDAY
    {
      timer.enable(TC);
      D1MainFunctionState = D1_48DOSEDAY;
      D1_State = 7;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
  }
}

a distinction between embedded and more conventional software is that embedded firmware often has different "modes" of behavior. a classic is aircraft: parked, taxiing, take-off, climb, cruise, landing.

each mode may have a different state machine governing it. a single monolithic state machine would be huge, but a small set of machines each handling the relevant stimuli is more manageable (both the develop, debug and maintain)

1 Like

As posted below my stimuli comes from a Firebase callback function. This function set which timing state i wish to use ie 1,2,6,12,24 or 48 times a day. Within the state machine under each of the timing states I have divided a total 1day in seconds by whatever timing state chosen. I the call a clock which gives me the unix time and do a modulo comparison, which is the stimuli to determine when to dose.

i believe your stimuli are your active flags being set inside your states

In the following example, a function pointer points to a function representing the current state. Within this function, the pointer can be updated to switch to the next state.

It should be functionally equivalent to the more classical approach, but I find it to be more manageable.

I had something like this already but it did not work. I had it so that when the modulo comparison was true call another function caller DoserFunction. This was blocking and the esp, and it did not work.

Ok that's interesting, you have just given me an idea

You want to go back to the first state after calculating the time of the next dose.

This entire snippet can be rewritten simply as
D1_BoostActive = value;
But I don't understand what's the purpose of such a function in a simple state machine.

Start following KISS: Keep It Simple, Stupid.

instead of trying to understand what your trying to do from your code, a clear, complete description would save time

First of thanks to alto777. You made me look at the code in a different light. My code has become shorter and also managed to get rid of extra functions.
Here is the revised code for anybody else.
Code which controls the State Machine (callback from my stream function for firebase)

void DOSETecStateFunction(String path, String value)
{
  if (path == "/D1_Boost")
  {
   if (value == "true")
   {
    D1_BoostActive = true;
   }
   else 
   {
    D1_BoostActive = false;
   }
  }
  else if (path == "/D1_CalibrateButton")
  {
    if (value == "true")
    {
      D1_CalibrateActive = true;
    }
    else
    {
      D1_CalibrateActive = false;
    }
  }
  else if (path == "/D1_ALKATecDose")
  {
    if (value == "true")
    {
      D1_ALKADoseActive = true;
    }
    else
    {
      D1_ALKADoseActive = false;
    }
  }
  else if (path == "/DOSETecActive")
  {
    if (value == "true")
    {
      DOSETecActive = true;
    }
  }
  //PARAMETERS//
  else if (path == "/D1_CalibrationInput")
  {
    D1_CalibrationInputFloat = value.toFloat();
    D1_DoseTime = 30000/D1_CalibrationInputFloat;
    EEPROM.put(10, D1_DoseTime);
    EEPROM.commit();
    Serial.println(D1_DoseTime);///////////////
  }
  else if (path == "/D1_DailyDosage")
  {
    D1_DailyDosageFloat = value.toFloat();
    EEPROM.put(5, D1_DailyDosageFloat);
    EEPROM.commit();
    Serial.println( D1_DailyDosageFloat);///////////////
  }
  else if (path == "/D1_SolutionInput")
  {
    D1_SolutionInputInt = value.toInt();
    D1_RemainVolume = D1_SolutionInputInt;
    EEPROM.put(15, D1_RemainVolume);
    EEPROM.commit();
    D1_SolutionFlag = true;
    D1_RemainVolFlag = true;
    Serial.println(D1_SolutionInputInt);//////////////////
  }
  //FUNCTIONS//////////////////////////////////////
  else if (path == "/D1_Function")
  {
    if (value == "0")   //OFF/BLEED
    {
      D1State = D1_IDLE;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "1")  //CALIBRATE
    {
      D1State = D1_IDLE;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "2")  //HOSECHANGE
    {
      D1State = D1_IDLE;
      D1_Hose = 0;
      D1_State = 0;
      EEPROM.put(25, D1_State);
      EEPROM.put(20, D1_Hose);
      EEPROM.commit();
      D1_HoseFlagAlert = true;
    }
    else if (value == "3")  //BOOSTMODE
    {
      D1State = D1_BOOST;
      D1_State = 1;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "4")  //1DOSEDAY
    {
      D1_Divisor = 1;
      ModDevisor = Mod1;
      D1State = D1_DOSETIME;
      D1_State = 2;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "5")  //2DOSEDAY
    {
      D1_Divisor= 2;
      ModDevisor = Mod2;
      D1State = D1_DOSETIME;
      D1_State = 3;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "6")  //6DOSEDAY
    {
      D1_Divisor = 6;
      ModDevisor = Mod6;
      D1State = D1_DOSETIME;
      D1_State = 4;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "7")  //12DOSEDAY
    {
      D1_Divisor = 12;
      ModDevisor = Mod12;
      D1State = D1_DOSETIME;
      D1_State = 5;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else if (value == "8")  //24DOSEDAY
    {
      D1_Divisor = 24;
      ModDevisor = Mod24;
      D1State = D1_DOSETIME;
      D1_State = 6;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
    else                    //48DOSEDAY
    {
      D1_Divisor = 48;
      ModDevisor = Mod48;
      D1State = D1_DOSETIME;
      D1_State = 7;
      EEPROM.put(25, D1_State);
      EEPROM.commit();
    }
  }
}

State Machine Code

//DOSER1///////////////////////////////////////////////////////////
void D1_MainFunction()
{
  switch (D1State)
  {
    case D1_IDLE:
    {
      if (D1_CalibrateActive)
      {
        digitalWrite(DOS1, HIGH);
      }
      else
      {
        digitalWrite(DOS1, LOW);
      }
      break;
    }
    if (D1_SolutionFlag)      //IF TRUE SOLUTION TO DOSE AVAILABLE
    {
      case D1_BOOST:
      {
        if (D1_ALKADoseActive)
        {
          //Put code here for the Alkatec
        }
        break;
      }
      case D1_DOSETIME:
      {
        EEPROM.get(10, D1_DoseTime);
        EEPROM.get(0, D1_DailyDosageFloat);
        D1_Time = ((D1_DailyDosageFloat / D1_Divisor) * D1_DoseTime) + Round;
        if (TimeNow % ModDevisor == 0)
        {
          D1_VolUpdateFunction(D1_Divisor, 0, false);
          D1_VolFlag = true;
          D1State = D1_DOSERON;
        }
        else if (D1_BoostActive)
        {
          digitalWrite(DOS1, HIGH);
        }
        else if (!D1_BoostActive)
        {
          digitalWrite(DOS1, LOW);
        }
        else
        {
        }
        break;
      }
      case D1_DOSERON:
      {
        D1_DoserTime = millis();
        digitalWrite(DOS1, HIGH);
        D1State = D1_DOSERTIMING;
        break;
      }
      case D1_DOSERTIMING:
      {
        if (millis() - D1_DoserTime >= D1_Time)
        {
          D1State = D1_DOSEROFF;
        }
        break;
      }
      case D1_DOSEROFF:
      {
        digitalWrite(DOS1, LOW);
        EEPROM.get(20, D1_Hose);
        D1State = D1_HOSECOUNT;
        break;
      }
      case D1_HOSECOUNT:
      {
        D1_Hose = D1_Hose + 1;
        EEPROM.put(20, D1_Hose);
        EEPROM.commit();
        D1State = D1_HOSECHECK;
        break;
      }
      case D1_HOSECHECK:
      {
        if (D1_Hose >= MaxHose && D1_HoseFlagAlert == true)
        {
          D1_HoseAlert();
        }
        D1State = D1_DOSETIME;
        break;
      }
    }
  }
}