2

New to FSM, can someone explain how the flow works? If I use a case scenario in a loop, how is an external change detected and brought back to the loop? Is all of the code/inputs, etc to be included within the case statements?

Still having a few problems. I have modified my code to include some of the suggestions. You can see that I added some print statements to allow me to monitor execution, but I can't get it to obey, such as print "trigger" when the PIR sensor is HIGH. Anyone available to explain my error(s)? I added a void check() just to bypass the keypad (not connected).

//////////////////////////// Initialize & Includes //////////////////////////
#include "SIM900.h"
#include "sms.h"
#include "Keypad.h"
#include <GSM.h>
SMSGSM sms;
//To change pins for Software Serial, use the two lines in GSM.cpp.
int PIR_SensorPin = 2;
int LED_OutPin = 11; //green
int Alarm_OutPin = 12; //red
int alarm_count = 0;
int switchPIN = 3; // alarm on test
char keypadPIN;
int z = 0;
int i = 0;
int waitTime = 5000;

//////////////////////////// Setup Keypad //////////////////////////
const byte ROWS = 4; //four rows
const byte COLS = 4; //four columns
char keys[ROWS][COLS] =
{
  {
    '1', '2', '3', 'A'
  }
  ,
  {
    '4', '5', '6', 'B'
  }
  ,
  {
    '7', '8', '9', 'C'
  }
  ,
  {
    '*', '0', '#', 'D'
  }
};
byte rowPins[ROWS] = {
  6, 7, A2, A3 // row pin# on keypad to Arduino pin#, ie Row Pin #1 goes to Arduino Pin #6, etc.
}; // connect to the row pinouts of the keypad
byte colPins[COLS] = {
  3, A0, A1, A4
}; // connect to the column pinouts of the keypad
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS );
const char PIN[5] = {
  '9', '6', '7', '9', '#'
}; // PIN number
char key_input[5] = {
  0, 0, 0, 0, 0
}; // used for comparison

//////////////////////////////// Setup ///////////////////////////////////
void setup() {
  Serial.begin (9600);  // Establish Serial connection;
  pinMode (LED_OutPin, OUTPUT);     // Set pinMode.
  pinMode (Alarm_OutPin, OUTPUT);   // Set pinMode.
  pinMode (PIR_SensorPin, INPUT);   // Set pinMode.
  digitalWrite (LED_OutPin, LOW);   // Set output pins to LOW for start.
  digitalWrite (Alarm_OutPin, LOW); // Set output pins to LOW for start.
  digitalWrite (PIR_SensorPin, LOW);
  Serial.println ("Studio Alarm starting up."); // Serial message that GSM shield is starting up
  if (gsm.begin(4800)) {    // Set GSM shield to recommended 4800 baud rate.
    Serial.println ("Status = Network READY");
    digitalWrite (LED_OutPin, HIGH);
  }
  attachInterrupt (digitalPinToInterrupt (2), trigger, CHANGE); // PIR as interrupt
  // Could make use of the interrupts by checking in the interrupt if the state is "Ready" and
  // then setting the state to "Alarm".
  Serial.println ("System Ready");

  keypadPIN = HIGH;
}

//////////////////////////// State Setup //////////////////////////
enum STATE {
  Ready, //system acknowledges trigger
  Unarmed, //system unarmed via keypad entry setting keypadPIN = LOW
  JustActivated, //PIR-triggered interrupt
  Alarm // confirm system unarmed then goto unarmed state else start alarm
};
STATE systemState = Unarmed;
long timeGettingArmed;

/////////////////////////// Trigger ///////////////////////////////
void trigger() {
  systemState = JustActivated;
}

//////////////////////////// State Machine //////////////////////////
void loop() {
  check ();
  switch (systemState) {Serial.println("switch");
    case Ready:
      if (PIR_SensorPin == HIGH) {Serial.println("PIR_Sensor");
        Serial.println("triggered");
        systemState = Alarm;
      } else if (keypadPIN == LOW) {
        systemState = Unarmed;
      }
      break;
    case Unarmed:
      if (keypadPIN == HIGH) {Serial.println("keypadPIN=HIGH");
        // Serial.println("Unarmed");
        timeGettingArmed = millis();Serial.println(timeGettingArmed);
         systemState = JustActivated;
      }
      break;
    case JustActivated:Serial.println("JustActivated");
      if (timeGettingArmed + waitTime < millis()) {
        Serial.println("Ready");
        systemState = Ready;
      }
      break;
    case Alarm:
      if (keypadPIN == LOW) {Serial.println("keypadPIN=LOW");
        systemState = Unarmed;
      } else {
        beep();
      }
      break;
  }
}

//////////////////////////// Alarm //////////////////////////
void beep() {
  Serial.println("at beep");
  // (sms.SendSMS("8656171435", "*** Motion Detected in Studio! ***"));
  Serial.println ("MOTION detected: SMS Sent");
  for (alarm_count = 0; alarm_count <= 5; alarm_count++) { // Cycle outputs if triggered
    digitalWrite (Alarm_OutPin, HIGH); delay (1000); // On/Off/On/Off/Off
    digitalWrite (Alarm_OutPin, LOW); delay (500);
    digitalWrite (LED_OutPin, HIGH); // delay (1000);
    Serial.println (alarm_count);
  }
  systemState = Ready;
}

void check() {
  if (switchPIN == HIGH) { 
    keypadPIN = LOW; // Unarmed
  }
  else {
    keypadPIN = HIGH; // Ready/Armed
  }
}
9
  • generally, you don't detect the change in that type of setup: you move one state to the next in a manner that that state behaves correctly after considering the context. for a button click/dblclick() library, i used: ready,1stDown,1stUp,2ndDown,stale states, which vary from the short,click,dblclick outputs. when in one state, check if you need to change to another state, and if not, do nothing. usually it's nothing.
    – dandavis
    Commented Apr 9, 2017 at 4:28
  • Maybe I wasn't clear. Are the inputs signaling an event change to change a state located within or outside of the case statements?
    – nkuck
    Commented Apr 9, 2017 at 4:50
  • one case, if needed, sets the state to another case or does nothing
    – dandavis
    Commented Apr 9, 2017 at 6:12
  • A "state" is just a variable. Anywhere that variable has scope can change the state.
    – Majenko
    Commented Apr 9, 2017 at 10:28
  • 1
    @Majenko: shared mutable state that gets changed in multiple places is the root of all evil, and the reason why redux/flux is so popular. I (and many others) HIGHLY recommend that you only change state in the switch to avoid spaghetti flow as the project grows. It's an adjustment for sure, but i loved it. ;)
    – dandavis
    Commented Apr 9, 2017 at 20:48

1 Answer 1

2

MCU State Flow

  1. State ( Model )
  2. User/Machine Output ( View )
  3. User/Machine Interruption ( Events )
  4. State Changers ( Controller )
  5. goto 1.

Breakdown of how it works:

#1. State. You start with a known finite state, which is the overall mode of operation for that piece or pieces. For a switch()-driven machine, it's a number, typically named by an integer constant for better readability.

#2. Output. Based on the state, you make certain things happen. These adjustments can be done in the switch(), or in the loop() as they notice state changes. You can also write explicit sub-routines to call from the switch() to keep code tidy. You can read the state from any point in the program, even interrupts if needed.

#3. Events/Interrupts. Lying in wait, these parts spring into action when something happens that the program needs to know. Both human-origin (ex: button press) and machine-origin (ex: SD card error) fall into this group. They can set flags and publish notifications to the controller that something has happened, but they MUST NOT set the state directly. The last point cannot be under-emphasized. If you start "cheating" and updating state outside of the controller, you create another point of failure and another stop along the debug/enhancement tour.

#4. State Changing switch() (Controller). This is where the actions meet the state. Only here should you change state to a new one, based on outside flags set by events. If your switch() is at the top of loop(), and you return in each case, you can call loop() from other points in the code to fire a state change immediately, but i like to name it in a void() and call it from loop(). The switch code should be lightweight and never use delay(). Lastly, most of the time a case does nothing, because no outside condition has changed that merits reaction, and it breaks or returns right there. When something important does change, the code below that early-exit conditional performs as a state change event handler.


Why bother

It can be a bit much at first, and seem like it's not worth the 5-10 mins up-front to name constants and write out all the switch bolierplate. I'm a lazy coder, and I still use this because it saves me a ton of time at every point in time besides those first 10 minutes.

Since things can only change in one place, you know exactly where to debug. It's simple to dump the outside values a case examines to the serial (remove before "launch"), rather than finding that chunk of code in the ether. Doing it all in one place makes it trivial to log when and what with each state change. With minor additional code you can even undo state changes, which can be a boon for users. Such actions are not risky because every part of the outside code knows what to do.

It breaks up tedious chains of semi-related action into independent pieces. You won't need lots of nested if()s if you follow the above guidelines. Even if you're not a long-time developer, simply following this pattern and slopping/pasting all the other code together, your program will be better in the end and faster and easier to build along the way.

1
  • Thanks. Looks like your idea is proving out well. Gave me another tool.
    – nkuck
    Commented Apr 15, 2017 at 6:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.