When to read sensors whilst running a state machine?

Hi, I’m trying to set up a process whereby once a part has been put into the machine certain actions are to be taken. For this I have created a state machine that runs good. In order to check the process I have included a few sensors, which I would like to keep updating and visible on screen throughout the process where I use some of them also for safety features.

If my understanding is correctly that a state machine runs as a loop within the ‘loop’ function (correct?). If I want the sensors to update continuously is it OK to have the related function calls in the ‘Loop’ function or would I need to repeat them in every state of the state machine? Thanks, Rene!

1 Like

Yes

In general yes. However, if the reading of the sensors takes a relative long time it might be better to only read them in a state where they are needed; that way you don't slow down other states.

Be aware that displaying data on a screen might take time and can therefore affect other parts of the code. It's in general advisable to only update a screen (whatever that is) when data changes or using a timed approach (e.g. only update every second). Note that this only applies to the displaying of the data, not the reading of the data.

1 Like

If you write non-blocking code, a state machine will run as a function in void loop() and you can have MANY of those that communicate through global variables. Each may have its own private variables, too, and I build a wait-timer in to replace delay()s with, all clean as the function generally does not take long before exiting until next loop().

Non-blocking code does not have inside loops except for very short ones and what has-to-be-done-before-void-loop-ends.

What I do is have an Input Task Function for every sensor or group of sensors (like button matrix), Output Task Functions for motors to serial text and Process Task Functions to process those algorithms and files, etc. For small things, I may merge process and output and run a 2 task system.

Some sensors don’t need to be read as often as possible, it’s up to the programmer to set read frequency and/or let the user do it. Analog read may take 50 to 109 or more millis during which the chip needs to be quiet and void loop() can be way faster than that except during the ADC so.. there’s realities to consider.

Thank You Rene and keep on coding!

I see it the opposite way. A state machine can be packed into a function that can be called from any number of places within your code. In detail that machine function can be called whenever your code waits for completion of some wait loop or between readings of sensors. Or put your sensor handling into another non-blocking state machine and call both functions within loop().

assuming your implementing your state machine as a switch statement, the common reading of sensors can be outside the switch statement while the case for each state would test for some event, possibly a specific sensor value resulting in a state transition

for example corected

#include "../Include/Arduino.h"

const int  PinSensor1 = 2;
const int  PinSensor2 = A2;
const int  PinOutput  = 10;

enum { S_Idle, S_Off, S_On };
int state = S_Idle;

const int ThreshHigh = 200;
const int ThreshLow  = 100;

// -----------------------------------------------------------------------------
void loop ()
{
    int  sensor1 = digitalRead (PinSensor1);
    int  sensor2 = analogRead  (PinSensor2);

    switch (state) {
    case S_Idle:
        if (LOW == sensor1)
            state = S_Off;
        break;

    case S_Off:
        if (HIGH == sensor1)
            state = S_Idle;

        else if (ThreshLow > sensor2)  {
            digitalWrite (PinOutput, HIGH);
            state = S_On;
        }
        break;

    case S_On:
        if (HIGH == sensor1)
            state = S_Idle;

        else if (ThreshHigh < sensor2) {
            digitalWrite (PinOutput, LOW);
            state = S_Off;
        }
        break;
    }
}

unsigned dbg;

int main ()
{
    loop ();
    return 0;
}

1 Like

How does case S_On ever run?

2 Likes

Fitting "for but not with" :smiley:

Thanks Team, really appreciate the support! One of the cases will involve an about 5 second process, which need to be protected for overheating and too much current draw so that would be the spot to call the sensor reading functions I think. Then again I need to see it cool down as well. Thinking cap on! Great comments, thanks! Brgds, Rene

not "as a loop" (no while or other iteration statement ).

Usually it's just one function call which implements one step of the machine. The loop() function is the one providing the "looping" which means that if nothing blocks the loop(), then your state machine function will be called again soon

void loop() {
  stateMachine();
  otherNonBlockingStuff();
  someMoreNonBlockingStuff();
}

and as other said, you can call it as often as you want even from the loop

void loop() {
  stateMachine();
  otherNonBlockingStuff();
  someMoreNonBlockingStuff();

  // block the loop for 5 seconds but keep the state machine ticking
  unsigned long t0 = millis();
  while (millis() - t0 <= 5000)  stateMachine();
}

Interesting! Reason I mentioned “about 5 sec” is that the process requires a part to be processed (:-)) for that amount of time. This process is controlled from the state machine. Basically simplified the state machine has 4 cases - waiting for a part (proximity sensor detection to start the process once part in place), Process the part (about 5 sec), process the part a bit more (about 1 sec), loose the processed part (servo operation). This overall process will be run between 50 to 500 times. I’m now starting to wonder if a state machine is the right structure to go with? Thanks, Rene

Loop one case inside of a state machine is a matter of not changing the state until it has been done X times. That needs an index/count variable (local static) and if(var >= X) to change the state. This is like the wait for start case except for a countdown.

Multi-case “loops” change the state value to the “loop” start case until finished else move on.

Process steps can run very flexibly to match changing conditions. Process states can be made of bits that describe those conditions.

Arduino Uno is capable of running loop() that does several tasks ~50+ times per millisecond. How close do you want to detect digital or analog events? An Input Task might read a button pin once every milli or 10 or 20 or 50 millis, depending on how much latency won’t make a difference much less hurt.

Independent sensor handlers take all the details code out of your Process Task(s) leaving them simpler.

You can write a (non-blocking) Task function in a small side-sketch and perfect it there, smaller and easier to debug than with the rest of the sketch and then add that to the project and if you planned well it is only a small debug for resource collisions or what with all the parts put together. It is good to build the project from prepared task functions in small sketches, ground up.

Then it is possible to make a set of “toolbox” sketches that interface with input and output hardware ready to support any process.

1 Like

If the machine has a number of separate 'states' it can be in then yes, a state machine will work. It's just a matter of substituting some timer condition for a sensor condition or value comparison.

Here's an example which uses switching and timing:

Many other examples on the forum, too.

The term state machine is used as a heading for event- and/or time-controlled and non-blocking data and signal processing.

Based on the description of the software functionality, a state change diagram should be created and programmed.

In short: there is no universal state machine.

Have a nice day and enjoy coding in C++. :slight_smile:

It is definitely suitable.

If you don’t have a sensor to triggger an event because it’s just « wait for 5s in that state » - that’s OK. Timeout can be an event too.

Here is an example, typed here - untested

enum State { WAITING_FOR_ACTIVATION, PROCESSING };
State currentState = WAITING_FOR_ACTIVATION;
constexpr byte sensorPin = 2;
unsigned long startTime = 0;

void stateMachine() {
  switch (currentState) {
    case WAITING_FOR_ACTIVATION:
      if (digitalRead(sensorPin) == LOW) {
        startTime = millis();
        Serial.println("Activated! Processing...");
        currentState = PROCESSING;
      }
      break;

    case PROCESSING:
      if (millis() - startTime >= 5000) {
        Serial.println("Processing done. Back to waiting.");
        currentState = WAITING_FOR_ACTIVATION;
      }
      break;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(sensorPin, INPUT_PULLUP);
  Serial.println("Waiting for activation");
}

void loop() {
  stateMachine();
}

Here is a small introduction to the topic: Yet another Finite State Machine introduction

a more common approach for a state machine function is to have an argument that is a stimulus, an event but this requires

the expiration of a timer can also be an external input, no need to implement a timer within the state machine


const int  PinSensor1 = A1;
const int  PinSensor2 = A0;
const int  PinOutput  = 10;

byte butState;

enum { S_Idle, S_Off, S_On };
int state = S_Idle;

const int ThreshHigh = 100;
const int ThreshLow  =  90;

unsigned long msecPeriod;
unsigned long msec0;
unsigned long msec;

bool          timeout;
bool          tmr;

bool sensor1;
int  sensor2;

// -----------------------------------------------------------------------------
void stateMach ()
{
    switch (state) {
    case S_Idle:
        if (sensor1)  {
            sensor1 = false;
            state = S_Off;
            Serial.println ("S_Idle -> S_Off");
        }
        break;

    case S_Off:
        if (sensor1)  {
            sensor1 = false;
            state = S_Idle;
            Serial.println ("S_Off -> S_Idle");
        }

        else if (ThreshHigh < sensor2) {
            digitalWrite (PinOutput, HIGH);
            tmr        = true;
            msec0      = msec;
            msecPeriod = 5000;
            state = S_On;
            Serial.println ("S_Off -> S_On");
        }
        break;

    case S_On:
        if (sensor1)  {
            sensor1 = false;
            state = S_Idle;
            Serial.println ("S_On -> S_Idle");
        }

        else if (ThreshLow > sensor2) {
            digitalWrite (PinOutput, LOW);
            tmr     = false;
            state   = S_Off;
            Serial.println ("S_On -> S_Off");
        }
        else if (timeout) {
            digitalWrite (PinOutput, LOW);
            timeout = false;
            state   = S_Off;
            Serial.println ("S_On -> S_Off - timeout");
        }
        break;
    }
}

// -----------------------------------------------------------------------------
void loop ()
{
    msec = millis ();

    if (tmr && msec - msec0 >= msecPeriod)  {
        tmr     = false;
        timeout = true;
    }

    sensor2 = analogRead  (PinSensor2);

    byte but = digitalRead (PinSensor1);
    if (butState != but)  {
        butState  = but;
        delay (30);

        if (LOW == but)
            sensor1 = true;
    }

#if (0)
    Serial.println (sensor2);
    delay (500);
#endif

    stateMach ();
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    pinMode (PinSensor1, INPUT_PULLUP);
    butState = digitalRead (PinSensor1);
}

this code drives the state machine with a stimulus, somewhat divorcing the state machine from recognizing events or knowing what type of event they are

const int  PinSensor1 = A1;
const int  PinSensor2 = A0;
const int  PinOutput  = 10;

byte butState;

enum { S_Idle, S_Off, S_On };
int state = S_Idle;

const int ThreshHigh = 100;
const int ThreshLow  =  90;


bool sensor1;
int  sensor2;

enum { St_Nul, St_Sensor1, St_ThrshHi, St_ThrshLo, St_Tmr };

// -----------------------------------------------------------------------------
unsigned long msecPeriod;
unsigned long msec0;
unsigned long msec;

bool          tmr;

void timerSet (
    int  _msecPeriod )
{
    msecPeriod = _msecPeriod;
    msec0      = msec;
    tmr        = true;
}

// -----------------------------------------------------------------------------
void stateMach (
    int  stim)
{
    switch (state) {
    case S_Idle:
        if (St_Sensor1 == stim)  {
            state = S_Off;
            Serial.println ("S_Idle -> S_Off");
        }
        break;

    case S_Off:
        if (St_Sensor1 == stim)  {
            sensor1 = false;
            state = S_Idle;
            Serial.println ("S_Off -> S_Idle");
        }

        else if (St_ThrshHi == stim) {
            digitalWrite (PinOutput, HIGH);
            timerSet (5000);
            state = S_On;
            Serial.println ("S_Off -> S_On");
        }
        break;

    case S_On:
        if (St_Sensor1 == stim)  {
            state = S_Idle;
            Serial.println ("S_On -> S_Idle");
        }

        else if (St_ThrshLo == stim) {
            digitalWrite (PinOutput, LOW);
            state   = S_Off;
            Serial.println ("S_On -> S_Off");
        }
        else if (St_Tmr == stim) {
            digitalWrite (PinOutput, LOW);
            state   = S_Idle;
            Serial.println ("S_On -> S_Idle - timeout");
        }
        break;
    }
}

// -----------------------------------------------------------------------------
void loop ()
{
    msec = millis ();

    if (tmr && msec - msec0 >= msecPeriod)  {
        tmr     = false;
        stateMach (St_Tmr);
    }

    sensor2 = analogRead  (PinSensor2);
    if (ThreshLow > sensor2)
        stateMach (St_ThrshLo);
    else if (ThreshHigh < sensor2)
        stateMach (St_ThrshHi);

    byte but = digitalRead (PinSensor1);
    if (butState != but)  {
        butState  = but;
        delay (30);

        if (LOW == but)
            stateMach (St_Sensor1);
    }

#if (0)
    Serial.println (sensor2);
    delay (500);
#endif

}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);

    pinMode (PinSensor1, INPUT_PULLUP);
    butState = digitalRead (PinSensor1);
}
1 Like

The state machine technique can be used in a large finite number of ways. In non-blocking code, every one of those is to be able to split up the steps/cases of the process so that it runs a short step and EXITS each time loop() runs… they ALL get a turn in a finely interleaved way, making for smooth automation.

The shorter the steps, the smoother it gets.

The difference between non-blocking code written as tasks and blocking code sequences is that the latter routines run serially, each must finish before the next begins while the former run effectively in parallel at the same time together —-

this is how you can have sensor+code-functions that don’t rely on any other code to work. Other code will use the sensor status/collected data without regards to how the sensor is read.

I do have an example sketch (which gets revised over good forum advice) that shows blocking vs non-blocking code that allows the user to switch which way runs and the serial monitor output shows the fundamental difference clearly. I will post it if you want.

1 Like

Wow, you just won the lottery. You have received the excellent advice of @GoForSmoke @gcjr @paulpaulson @dougp and @J-M-L in one thread. I have already copied all their samples and links etc and will study them in order to produce better code. I hope you post your finished project so you help future folks with the same question.

2 Likes

I thought with your experience that you already knew what I wrote!

Different approach?

Like me, @sonofcy probably has his crystal ball away for certification and calibration.

I've been following and its almost a tutorial, with down to earth examples.

Well done to all. :+1: :+1:

Tom.... :smiley: :+1: :coffee: :australia:

2 Likes