Learning about FSM: what are the ‘states’ here?

I’m finding FSM heavy going despite studying several tutorials and example sketches. My main hang up is: how to reliably define the states for any specific circuit requirement?

As a concrete example I’d much appreciate knowing what states I should list for the sketch discussed in my thread ‘Strugging with millis() for two tasks’

My sketch below is heavily commented, so hopefully will give a clear picture. Only two tasks are involved, and the sketch works as it should after much help. But I am keen on mastering FSM with Switch & Cases so would welcome any help please.


// Monday 13 September 2021
// About to move 328 off UNO and onto independent veroboard so made some changes
// Yellow (not blue) LED now lit from D13, not D7
// Kept the serial output for testing
// But need to be able to test when independent.So added switch 
// to circuit case; pin D4 input from switch sets KA interval to 5s (for testing) or 160s
// Takes 'keep alive' (KA) photo with one servo press every 2m40s (160s)
// The low-going leading EDGE of the 20s outputfrom motion sensor is D6 input.
// That's the major change from LED emulation version
// It causes two servo presses on CUBE button, starting a video recording (and lighting red LED as an indicator, now from D13)

#include <VarSpeedServo.h> // Uses the VarSpeedServo library
VarSpeedServo cubeservo;

// Constants
const byte triggerPin = 6; // Low input from button to D6
const byte yellowLEDPin = 13; // LED flashes at intervals for KA photo
const byte redLEDPin = 8; // Lit while video recording
const byte servoOutPos = 30; // Trial & error
const byte servoInPos = 10; // Trial & error
const byte switchPin = 4; // For easier testing when on Veroboard not UNO
const int waitBeforeRelease = 200;
const int waitAfterRelease = 200;

// Variables
int triggerCounter = 0; // Counts number of triggers (MS lows)
int KACounter = 0; // Counts number of KA photos
bool trigState = HIGH;
bool prevtrigState = HIGH;
unsigned long previousMillis = 0; // Effectively the program start time
unsigned long interval; // 5s (testing) or 160s (2m40) KA photos interval

void setup()
{
  pinMode(switchPin, INPUT_PULLUP);
  pinMode(triggerPin, INPUT); // D6
  pinMode(redLEDPin, OUTPUT); // D8
  pinMode(yellowLEDPin, OUTPUT); // D13
  Serial.begin(115200);
  Serial.println("ServoCUBEvideosMS-3");
  Serial.println("");
  cubeservo.attach(9); // CUBE servo (white wire)
  cubeservo.write(servoOutPos);
  delay(500);

  // Test D4 to select value of interval
  if (digitalRead(switchPin) == HIGH)
  {
    interval = 160000; // 2m40s
  }
  else
  {
    interval = 5000; // 5s
  }
//    while(1 == 1); // For testing. Stops program entering loop
}

void loop()
{
  // Input to D6 is from a motion sensor (MS), which is normally H
  // It goes L when motion detected, returning H about 18-20s later.
  // Get the current input state
  trigState = digitalRead(triggerPin);
  // Compare with its previous state
  if (trigState != prevtrigState)
  {
    if (trigState == LOW) // Implies this is the low-going edge we want
    {
      triggerCounter++;
      Serial.print("triggerCounter = ");
      Serial.println(triggerCounter);
      // NOW TAKE ALL THE ACTIONS NEEDED AFTER TRIGGERING
      Serial.print("Started video ");
      Serial.println(triggerCounter);

      // Move servo arm in/out twice to press CUBE button twice
      singleCUBEServoPress(); //Single short sweep in and out
      singleCUBEServoPress(); //Single short sweep in and out

      digitalWrite(redLEDPin, HIGH); // Indicates recording in progress
      //Stays high during video recording, i.e for 30s
      //      delay(4000); // For testing
      delay(30000); // 30s video
      digitalWrite(redLEDPin, LOW); // Stays low until next video record

      // Now end video recording with a single CUBE button press
      singleCUBEServoPress(); //Single short sweep in and out
      Serial.print("Ended video ");
      Serial.println(triggerCounter);
    }
    // Nothing needed otherwise, i.e no else() required here??
    // But will include for reporting at least for now
  }
  //  else
  //  {
  //    // if the current state is HIGH then the button went from L to H:
  //    Serial.println("Input went from L to H");
  //  }

  // Brief delay (as seen in others' sketches), although could probably
  // be even shorter (1ms for stability?) as there is no bouncing involved
  delay(100);
  // Save current state for next time through the loop
  prevtrigState = trigState;
  // End of trigger detection and actions

  /////////////////////////////////////////////////////////////////////

  // CUBE camera inactivity for about 3 mins powers it off. So must take
  // 'keep alive' (KA) photo before this
  // Check to see if it's time for a KA photo
  unsigned long currentMillis = millis(); // Get current time
  if (currentMillis - previousMillis >= interval)
  {
    // Move servo arm in/out once to press CUBE button once
    singleCUBEServoPress(); //Single short sweep in and out - function below

    digitalWrite(yellowLEDPin, HIGH); // (was blue)
    delay(500); // Brief flash
    digitalWrite(yellowLEDPin, LOW);
    
    KACounter++; // Increment
    Serial.print("KA photo ");
    Serial.println(KACounter);
    Serial.println("");
    previousMillis = currentMillis; // Reset
  } // End of KA code

} // End of Void Loop

void singleCUBEServoPress()
{
  cubeservo.write(servoInPos); // Rotates to the 'In' position
  delay(waitBeforeRelease); // Duration of press; was 200
  cubeservo.write(servoOutPos); // Rotates to the 'Out' position
  delay(waitAfterRelease); // Duration of release/recovery; was 200
}

I would say that a good place to start is not from the code but from a description of what you want to do. Then you define 'states' based on that description and write code that fits. Happy to help you out if you want, but you will need to write the description.

You may already understand this already, but you may find this blog and its follow up post helpful https://arduinoplusplus.wordpress.com/2019/07/06/finite-state-machine-programming-basics-part-1/

1 Like

i'm confused by what the code appears trying to do.

there are two time driven operations. the one performs an operation periodically, every 16 sec. but a 2nd operation triggered by a button involves a 20 sec operation that prevents the first (16 sec) from occurring.

a sequence of actions occurring with a varying amount of time between each can be driven using a millis() condition and a sequence number.

but it's not clear how this approach can be applied with the conflicting operations of the posted code

do you mean this?

sorry I couldn't help myself.

3 Likes

Multitasking is a special case of state machine. In this case, the states are loop() states, i.e., they control which portions of code inside loop() are repeatedly executed. So each state is in fact, a state of repetition. This provides the illusion of concurrency from a sequential logic flow.

It may make more sense to study a complete example that has nothing to do with your current undertaking.

The classic is traffic lights / intersection control, good examples will go step by step through the specification of the states to the means and mechanisms for stepping among them.

Your CUBE program might end up being two state machines, each getting a call every time through your loop(), neither doing anything most of the time.

So you got yourself a twofer there, FSM and using them to do multitasking.

I’ve said before it’s hard to argue with success. Your other code will be deplorable deployable or is already; but you have seen how the simpler approach can result in unintended consequences.

Part of but not unique to FSM design is that it sorta forces you to consider all the possibilities. By the time you are actually writing code, there are no questions left, and no surprises come when you fire it up.

Ideally. :expressionless:

It’s one of those spend a minute now and save an hour and maybe some of what’s left of you hair later. And routinely not done no matter we should have learned - there’s a bias or hubris to cranking out the code and expect that it will just work.

Step by step, or state by state, take your time and look forward to a very valuable technique added to the tool bag.

Oh, IMO you woukd do better avoiding anyone’s library or such like. Mastering use of then is often more trouble than it is worth. Personally I want to write this kind of thing for myself, although at this point I have plenty of my own code to leverage.

HTH

a7

1 Like

State machines have no stack, so there is no 1:1 translation from nested code. So the simple stuff works easily, but things can get messy if you have complicated nested logic. Then you need to fake a stack or use multiple state sets. It's kind of the "emperor's new clothes" of multitasking approaches.

Yes.

I should not have used a big word to mean that if you have a few things that each can accomplished with an FSM, you can make them all progress at what seems like the same time simply by giving them frequent attention.

a7

i believe in this case, the OP is probably thinking of a sequencer, a state machine the advances sequentially thru a set of state, probably based on timing

rather that a 3 dimensional FSM that could transition between any two states depending on a number of stimuli and processing unique actions based on the state or even the transitions.

Yes, like a PLC

looks like a PLC is pretty flexible, controlled from multiple inputs.

the sequencer i tried describing, would be driven by timer expirations and possibly a single button press that starts a sequencer, which is what i believe the OP has in mind

Offered for your consideration: State machines

1 Like

I support that.
Discribe the functionality of your FSM.
Paint a diagram.

A diagram will lead you to a good solution.

Many thanks for all the responses so far. As a result my next step is to study the examples from both @marco_c's blog which I'd missed, and @dougp's garage opener, which I had found last year when I first dipped a toe in FSM waters, but now need to work through carefully. These seem hopeful sources to overcome the main hurdle I described: "how to reliably define the states for any specific circuit requirement?"

Staying with my specific simple example project (now working), I'm keen to learn the methodical thought processes I should take in order to define its 'states'. So that they could be used in a series of Switch/case blocks. IOW, asssuming the state definitions accurately reflected the real life requirements (the hard part), the coding should hopefully be virtually automatic (the no-brainer part)!

I'm a 'devil is in the details' guy. However given my detailed code comments, and the parallel thread discussion, I didn't want to bore through repetition - but see that was a mistaken judgement! So here's a full summary of the project.

I have a CUBE camera with one button operation.

For example, once it's powered up (which my sketch assumes it is), a single press and release (I'll call these 'presses' from now on) will take a photo and store it on its micro-SD card. And two presses will start a video. If neither of these actions occur within 3 minutes then it will power down. So my program ensures it avoids that by taking what I call a 'keep alive' (KA) photo. The alternative of powering up the CUBE for every video takes far too long; the target, e.g. a nocturnal fox, would probably be long gone before my video recording started.

The interval between KA photos is 2m40s (160s). For testing this is reduced to 5s. D4 is wired through a switch and used during setup to assign the duration (high => 160, low => 5).

When a photo is taken, a yellow LED is lit for 500ms.

A video recording is triggered when a motion sensor (MS) detects movement. Its signal is low going from 5V to nearly 0V and lasts about 20 seconds, so my program detects only the leading edge.

When a video is being recorded, a red LED is lit throughout.

A servo is used to press the CUBE button. So a single 'in/out' arm rotation of 20º takes a photo; two starts a video; and another single then ends the video. Its duration is set for 30s, (unchanged for testing).

Screenshot before running program

During program setup the arm is moved to its required start position:

Terry

Dealing with FSMs is trivial - once you understand them but sadly, gaining that comprehension is really quite hard. It's puzzling, once it clicks, it's hard to see what was so difficult about them, but everyone (with the possible exception of mathematicians) seems to have to go through a longish head scratching phase.

The only reliable way to get there I think is to look at as many examples as possible and then start writing some.

Another bear-trap with these things is that eventually, you see the light and you've conquered it. You are a master of FSMs. Then you try and tweak one a bit and you discover that your mastery was actually a bit (very) superficial and you still don't really understand them. More examples, more coding are the cure for this too :wink:

2 Likes

As far as your project is concerned, I read your description but not your code and I see three states: STARTUP, KEEP_ALIVE and FILMING.

Startup may be unnecessary, but it would make it easy to allow some time for the camera to power up and to take the first keep alive picture and note when it was taken.

Keepalive monitors the movement sensor and the time since the last picture. When a picture is warranted, it takes one and notes the time. On movement, it starts the video, notes the time and goes to filming state.

In filming state, there's really nothing to do but count thirty seconds and return to keepalive.

I'd ignore the leds for the first iteration.

not sure this is much of a FSM.

seems like the code just needs a timer event to perform some action.

by default, the time is set to expire every 3- minutes and perform an operation to take a snapshot.

when the trigger is set, the timer is set to expire in 30 secs and a state variable set to indicate the action is to end the video and reset the timer to expire in 3- mins and the state variable for a snaphot

consider

#undef MyHW
#ifdef MyHW
const byte trigPin   = A1;
const byte yellowLEDPin = 12;
const byte redLEDPin    = 13;

#else
const byte trigPin   = 6; // Low input from button to D6
const byte yellowLEDPin = 13; // LED flashes at intervals for KA photo
const byte redLEDPin    = 8; // Lit while video recording
#endif

enum { Off = HIGH, On = LOW };

const int WaitBeforeRelease = 200;
const int WaitAfterRelease  = 200;

byte trigPinState;
int  trigCounter = 0;

#define VideoTime   15000;
#define Snapshotime 20000;

unsigned long msecExpire;
unsigned long interval = 15000;
unsigned long msec;

enum { ST_SNAPSHOT, ST_VIDEO };
int state = ST_SNAPSHOT;

// -----------------------------------------------------------------------------
void
singleCUBEServoPress (
    int cnt )
{
    while (cnt--)  {
        digitalWrite (yellowLEDPin, On);
        delay (WaitBeforeRelease);

        digitalWrite (yellowLEDPin, Off);
        delay (WaitAfterRelease);
    }
}

// -----------------------------------------------------------------------------
void
snapshot (void)
{
    singleCUBEServoPress (1);
}

// -----------------------------------------------------------------------------
void
videoStart (void)
{
    singleCUBEServoPress (2);

    trigCounter++;

    Serial.print ("Started video ");
    Serial.print ("trigCounter = ");
    Serial.println (trigCounter);
    digitalWrite (redLEDPin, On);

    msecExpire = msec + VideoTime;
    state = ST_VIDEO;

    digitalWrite (redLEDPin, LOW); // Stays low until next video record
}

// -----------------------------------------------------------------------------
void
videoEnd (void)
{
    singleCUBEServoPress (1);
    Serial.println (" End video ");
    digitalWrite (redLEDPin, Off);
}

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

    if (msec > msecExpire)  {
        switch (state)  {
        case ST_VIDEO:
            videoEnd ();
            state      = ST_SNAPSHOT;
            msecExpire = msec + interval;
            break;

        case ST_SNAPSHOT:
        default:
            snapshot ();
            msecExpire = msec + interval;
            break;
        }
    }

    byte trig= digitalRead (trigPin);
    if (trigPinState != trig) {
        trigPinState = trig;

        if (trig == LOW) 
            videoStart ();
    }
}

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

    pinMode (trigPin,   INPUT_PULLUP);
    trigPinState = digitalRead (trigPin);

    pinMode (redLEDPin,    OUTPUT);
    pinMode (yellowLEDPin, OUTPUT);
    digitalWrite (redLEDPin,    Off);
    digitalWrite (yellowLEDPin, Off);

    msecExpire = interval;
    state      = ST_SNAPSHOT;
}

Indeed, as I was defining mine, it became clear that almost everything lives in a single state. It does make a nice simple introductory example though.

@gcjr Nice. One small change, since I had to go looking for it, move the state change into the switch case, viz:

   if (trig == LOW) {
      state = ST_VIDEO;
      videoStart ();
   }

Still remains the possibility of a video trigger coming in in the heels of a keep alive photo. I do not know how much time must elapse after a photo is taken before a video can be started.

a7

then msecExpire should also be set along with it

but why there and not videoStart()?