Code design: is State Machine relevant for my need?

Hello,
tl;dr;
What's the best approach to code such a state hierarchy?

  • normal mode
    • waiting for the night
    • the night (waiting for the day)
      • waiting for On signal
      • fade in led
      • fully on led (waiting for Off signal)
      • fade out led
      • waiting for light Off delay
  • admin mode
    • debug
    • pairing
    • reset

Context:

  • This is an abstraction of a project I've been working on for a while now. I have an existing "spaghetti style" code base which works quite well, but it's tiresome to update and modify.
  • It is battery operated, so I try to sleep my uC as much as possible, with different duration depending on modes (like very long for the state "waiting for the night", very short for "fade in led").
  • I have a complete chart diagram that I can share if it helps, but it may be too specific and complex.

Many thanks!

It would be useful to understand a bit more about your project.

For example what inputs are there that control moving between states?

Can you jump from any state to any other state - for example, from Normal Mode to Admin Mode from any of the Normal sub-states?

The flow chart would be useful.

Here's the flowchart I did: Dropbox - Screenshot 2022-06-19 at 11.38.18.png - Simplify your life
Sorry if it's messy/incomplete, I'm no software engineer :slight_smile:

Some are timers, some are input digital pins, some analog pins.

Yes! That's what worries me :slight_smile:

it can be done with a state-machine. And I expect that it will be easier to maintain than without a state-machine.

The advantage of a state-machine is that the number of required if-conditions is reduced through the switch-case statement.

If you decide to modify your code for beeing based on a statemachine I highly recommend to use self-explaining names for everything.

try to understand this code-version

#include<millisDelay.h>
int light1=22;
int exhaust =23;


int pir1 =26;
int pir2=27;


int pirState1;
int pirState2;

millisDelay exhaustDelay;
unsigned long delayTime = 1*60*1000;
int flag =1;
int flag2=1;

void washRoomLight()
{
  if(pirState1==1 || pirState2==1 && flag==1)
   {
    digitalWrite(light1,HIGH);
    flag=0;
    }
   if(pirState1==0 && pirState2==0 && flag==0)
   {
    digitalWrite(light1,LOW);
    exhaustDelay.start(delayTime);
    flag=1;
    } 
  }
void exhaustFan()
{
  if(pirState2==1 && flag2==1)
  {
    digitalWrite(exhaust,HIGH);
    flag2=0;
    }
   if(pirState2==0 && flag2==0 && exhaustDelay.justFinished())
   {
    digitalWrite(exhaust,LOW);
    flag2=1;
    } 
  }
   
void setup() {
  // put your setup code here, to run once:

pinMode(pir1,INPUT);
pinMode(pir2,INPUT);


pinMode(light1,OUTPUT);
pinMode(exhaust,OUTPUT);

}

void loop() {
  // put your main code here, to run repeatedly:
pirState1 = digitalRead(pir1);
pirState2 = digitalRead(pir2);
washRoomLight();
exhaustFan();
}

pretty hard to almost impossible as the names are not self-explaining

Now read this code-version which has the same functionality

#include<millisDelay.h>
const byte lightWashRoom_Pin = 22;
const byte exhaust_Pin       = 23;


const byte pirWashRoom_Pin = 26;
const byte pirToilet_Pin   = 27;

const byte personDetected = 1;
const byte notActivated   = 0;

byte  pirWashRoomState;
byte pirToiletState;

#define OFF HIGH
#define ON  LOW


millisDelay exhaust_PinDelay;
unsigned long delayTime = 1 * 60 * 1000;

boolean lightSwitchedOn = false;
boolean FanIsRunning    = false;


void washRoomLight() {

  if (!lightSwitchedOn) {
    if (pirWashRoomState == personDetected || pirToiletState == personDetected ) {
      digitalWrite(lightWashRoom_Pin, ON);
      lightSwitchedOn = true;
    }
  }

  if (lightSwitchedOn) {
    if (pirWashRoomState == notActivated && pirToiletState == notActivated ) {
      digitalWrite(lightWashRoom_Pin, OFF);
      lightSwitchedOn = false;

      exhaust_PinDelay.start(delayTime);
    }
  }
}


void exhaustFan() {
  if (!FanIsRunning) {
    if (pirToiletState == personDetected ) {
      digitalWrite(exhaust_Pin, ON);
      FanIsRunning = true;
    }
  }

  if (FanIsRunning) {
    if (pirToiletState == notActivated && FanIsRunning && exhaust_PinDelay.justFinished()) {
      digitalWrite(exhaust_Pin, OFF);
      FanIsRunning = false;
    }
  }
}


void setup() {
  // put your setup code here, to run once:

  pinMode(pirWashRoom_Pin, INPUT);
  pinMode(pirToilet_Pin, INPUT);


  pinMode(lightWashRoom_Pin, OUTPUT);
  pinMode(exhaust_Pin, OUTPUT);

}

void loop() {
  // put your main code here, to run repeatedly:
  pirWashRoomState = digitalRead(pirWashRoom_Pin);
  pirToiletState   = digitalRead(pirToilet_Pin);
  washRoomLight();
  exhaustFan();
}

pretty easy as all names are self-descriptive
best regards Stefan

it will be possible in multiple ways.

one approach would be to have two state-machines
normal and admin
if you have an if-condition above both state-machines
this if-condition will control which state-machines will be executed

if (DigitalRead(Normal_Admin_Switch == HIGH) ) {
  admin_mode = true;
}
else {
  admin_mode = false;
}

and calling the state-machines
is conditional to that

if (admin_mode) {
  admin_FSM();
}
else {
  normal_FSM();
}

maybe you have to add some initialisation of the state-variables

I try to do this, but never thought of defining ON/OFF like that, will try to implement that when I don't have NPN and PNP on the same hardware :stuck_out_tongue:

Ok, I will do that, but I hope I wont get lost again in a bunch of cumbersome "if else"!

it seems your example has two separate "modes", each with separate set o stimuli. in the one mode, the behavior follows a sequence of states. in the "admin" case, the code jumps between a few sub-states.

in both cases, i would suggest using a state variable or flag to keep track of the sub-state. separate switch statements may be more than adequate for you normal and admin cases

define it this way is for low-active relays.
low-active means relay is powered and normally opened contact is then closed if output-pin is set to low.

no you will not because of the switch-case-break-statement
The break is very important. Because the break makes each case mutually exclusive

This is the main reason why a switch-case-break-statement reduces the number of if-conditions.
Only that part of the code gets executed that is inside this particular state the state-variable is set to.

All other cases are skipped through the break; This means code in the other cases is will not be executed.

best regards Stefan

Thank you @red_car, @StefanL38 and @gcjr for your answers!
Here's the bare-bone of the state machine I'm going to work from:

void loop() {
  switch (globalState) {
     case NORMAL:
        if (isItAdmin()) {
          state = ADMIN;
        } else {
          switch (luminosityState) {
            case THE_NIGHT:
              if (!isItNight()) {
                state = WAITING_FOR_THE_NIGHT;
              } else {
                switch (luminosityState) {
                  case WAITING_FOR_ON_SIGNAL:
                  
                  break;
                  case FADE_IN:
                  
                  break;
                  case FULLY_ON:
                  
                  break;
                  case FADE_OUT:
                  
                  break;
                  case LIGHT_OFF_DELAY:
                  
                  break;
                }
              }
            break;
            case WAITING_FOR_THE_NIGHT:
              if (isItNight()) {
                state = THE_NIGHT;
              } 
            break;
          }
        }
        break;
      case ADMIN:
        if (!isItAdmin() {
          state = NORMAL;
        } else {
          switch (luminosityState) {
            case DEBUG:
                      
            break;
            case RX_PAIRING:
                      
            break;
            case RX_RESET:
                      
            break;
          }
        }
        break;
    }
}

Does it looks like a good start?
Many thanks!

Don't get too carried away with the concept of a 'State Machine".. at the highest level you are either in Admin or Normal mode... a simple "if' statement will do that. I think you're making things a bit more complicated than they need to be.

void loop()
{
  if (isItAdmin())
    //do Admin stuff;
  else
    //do Normal stuff;
}

Similarly, THE_NIGHT and WAITING_FOR_THE_NIGHT, can be done with a simple "if". State machines are really only required when you have more complex sequences of activities.

You're right @red_car it looks much simpler! :pray:

void loop() {
  if (isItNormal()) {
    if (isItNight()) {
      switch (ledsState) {
        case WAITING_FOR_ON_SIGNAL:
                 
        break;
        case FADE_IN:
                 
        break;
        case FULLY_ON:
                
        break;
        case FADE_OUT:
                 
        break;
        case LIGHT_OFF_DELAY:
                 
        break;
      }
    } else { // !isItNight()
      
    }
  } else {  // !isItNormal()
      switch (adminState) {
        case DEBUG:
                      
        break;
        case RX_PAIRING:
                   
        break;
        case RX_RESET:
                   
        break;
      }
    }
  }
}

Simple is good.

1 Like

It's the whole point of my initial question :slight_smile:
But I find it surprisingly difficult to find information about when to use some programming concept and when not to use it... Like: should I use object oriented programming? Should I split my code into multiple files? etc. I never know when my project will really benefits from those more advanced ways of programming...

To me it's still a state machine, whether you use switch...case or if to implement it. A state machine is an approach, a paradigm. It can be implemented in a number of ways; the key surely is that one thinks in terms of states. Once you do that, and can draw a "standard" style state diagram, surely it's irrelevant how you code the detection of which state you're in, and the management of the moving from state to state.

If you can draw it like this:

image

... it's a state machine, no matter what you do under the hood. To me, anyway.

I get your point, but as my initial question is how to simplify my code to make it easier to read, the "under the hood" things are my primary concern :stuck_out_tongue:

I was replying to @red_car though; guess I forgot to click the right "reply" icon :wink:

I also get your point... and agree that pretty much all code has an element of state. I guess when I hear the term "state machine" a think of a mechanism that is required, over and above simple decision statements that direct the flow of code.

1 Like

That's called experience... and it comes with doing, trying, failing, learning.

1 Like

the best way to learn is to see well written programs. i learned a lot from The Elements of Programming Style which debugged textbooks examples of programs

you should consider OO techniques, but you don't need to use "classes" to write an OO program which can be implemented in assembly.

OO is about organizing data and functions operating on that data that makes your application easier to understand and implement

but is one or multiple state machines?

imagine the size of the matrix based on the total # of states and stimuli. then consider if some stimuli only affect some states and if that sub group can be a separate state machine.

consider the total sizes of separate state machines or sequencers compared to the a single state machine matrix and the ability to maintain (read, modify, enhance) them

1 Like