(First Draft) Several Things in Succession (a simple State Machine Tutorial)

This is the first draft of this Tutorial and all comments are welcome. After about a week or so I will produce an updated version taking account of comments where appropriate.

I would particularly welcome comments about how to make the code more obvious for a newbie or about improvements to clarify my explanation.

Introduction

In Several Things at a Time I showed how to write a program so that it appears that the Arduino is doing things simultaneously. Actually it can only do one thing at a time but because it is very fast by human standards it can give the impression that multiple things are happening at once.

There is another sort of programming problem in which the various tasks depend on previous actions having been completed. For example the warehouse door must be opened before the forklift can drive in to pick up a load. In some projects the different tasks may take a known length of time and a program could be written to do different things at different time intervals. But in many real world situations there is no predictable duration for an action. For example on one occasion the forklift driver may spend a few minutes chatting with a colleague before exiting the warehouse so a door closure based on a timer would not be suitable.

The usual way to deal with this sort of problem is with the concept that has the rather grand name of "State Machine". Don't worry, this is not a complicated programming idiom that will require 2 or 3 weeks of careful study. All that's involved is the use of one or more variables to keep track of the state of the system, or different parts of the system.

Applying a State Machine to the problem

For my example program I will use the variable doorState to keep track of the door and forkliftState to keep track of the forklift. (I use a servo to represent the door and flashing LEDs to represent the forklift moving and the load being picked up and a push-button switch to start things off).

The doorState can take any one of these values
LOCKED,
UNLOCKED,
OPENING,
OPEN,
CLOSING,

The idea is that the forklift can't move unless the doorState is OPEN and the door cannot operate unless the forklift is STOPPED. That way the forklift should never hit the door and the door should never hit the forklift.

The values for forkliftState will be
STOPPED,
ENTERING,
LOADING,
READY_TO_LEAVE,
LEAVING,

In the program I have represented these states using ENUMs. If you are not familiar with them I have written a few words of explanation at the bottom of Reply #XX.

The general idea of a State Machine is that the program moves from one state to the next as different things happen. For example when the door is locked a push on the START button will change the state to UNLOCKED. That almost clears the path for the door to open but the program must also check to ensure that the forklift is STOPPED. Assuming it is the state will change to OPENING and that will allow the servo to operate. When the servo has reached the end of its sweep doorState will change to OPEN.

All this time the forklift will have been waiting but it cannot start moving until the doorState is OPEN. (For simplicity I am just assuming the forklift takes a set time to enter or leave the warehouse. In a real situation the location of the forklift might be detected by a beam-break sensor at the warehouse door)

When applying the State Machine concept to a problem it is important to appreciate that what happens at any one time depends only on the value in the state variable. For example the START button does not directly make the door move, it only changes the value of doorState from LOCKED to UNLOCKED. Separating the inputs from the outputs in this way means that the checkButton() function could be replaced by (for example) a function that gets a start message from the Arduino Serial Monitor without any changes being needed in other parts of the program. The subsequent actions neither know nor care how doorState comes to have the value UNLOCKED.

It is probably also worth pointing out how the use of state variables allows each function to be very short. That makes debugging much easier and allows for easy testing of the functions separate from one another if necessary.

The Program

// python-build-start
// action, upload
// board, arduino:avr:uno
// port, /dev/ttyACM0
// ide, 1.8.6
// python-build-end

#include <Servo.h>

enum doorStateENUM {
    LOCKED,
    UNLOCKED,
    OPENING,
    OPEN,
    CLOSING,
};

doorStateENUM doorState;

enum forkliftStateENUM {
    STOPPED,
    ENTERING,
    LOADING,
    READY_TO_LEAVE,
    LEAVING,

};

forkliftStateENUM forkliftState;

Servo doorServo;

unsigned long forkliftStartTime;
unsigned long loadStartTime;
unsigned long forkliftMovePeriod = 2000;
unsigned long loadingPeriod = 2000;

byte startButtonPin = 9;
byte forkliftLedPin = 10;
byte loadingLedPin = 7;
byte doorServoPin = 8;

int servoMin = 1000;
int servoMax = 2000;

//=========

void setup() {
    Serial.begin(115200);
    Serial.println("Starting StateMachineDemo");

    pinMode(startButtonPin, INPUT_PULLUP);
    pinMode(forkliftLedPin, OUTPUT);
    pinMode(loadingLedPin, OUTPUT);
    digitalWrite(forkliftLedPin, LOW);
    digitalWrite(loadingLedPin, LOW);
    doorServo.attach(doorServoPin);
    doorServo.writeMicroseconds(servoMin);

    doorState = LOCKED;
    Serial.println("DOOR LOCKED");
    forkliftState = STOPPED;
    Serial.println("FORKLIFT STOPPED");

}

//=========

void loop() {
    checkButton();
    checkMayDoorMove();
    operateDoor();
    checkMayForkliftMove();
    moveForklift();
    loadForklift();
}

//=========

void checkButton() {
    byte buttonState = digitalRead(startButtonPin);
    if (doorState == LOCKED and forkliftState == STOPPED) {
        if (buttonState == LOW) {
            doorState = UNLOCKED;
            forkliftStartTime = millis();
            Serial.println("DOOR UNLOCKED");
        }
    }
}

//=========

void checkMayDoorMove() {
    if (forkliftState == STOPPED) {
        if (doorState == UNLOCKED) {
            doorState = OPENING;
            Serial.println("DOOR OPENING");
        }
        if (doorState == OPEN) {
            doorState = CLOSING;
            Serial.println("DOOR CLOSING");
        }
    }
}

//=========

void operateDoor() {
        // servo simulates door opening
    if (doorState == OPENING or doorState == CLOSING) {
        bool moveComplete = false;
        if (doorState == OPENING) {
            moveComplete = updateDoorServo('O');
            if (moveComplete == true) {
                doorState = OPEN;
                Serial.println("DOOR OPEN");
            }
        }
        if (doorState == CLOSING) {
            moveComplete = updateDoorServo('C');
            if (moveComplete == true) {
                doorState = LOCKED;
                Serial.println("DOOR LOCKED");
            }
        }
    }
}

//========

void checkMayForkliftMove() {
    if (forkliftState == STOPPED or forkliftState == READY_TO_LEAVE) {
        if (doorState == OPEN) {
            if (forkliftState ==  STOPPED) {
                forkliftState = ENTERING;
                Serial.println("FORKLIFT ENTERING");
            }
            if (forkliftState == READY_TO_LEAVE) {
                forkliftState = LEAVING;
                Serial.println("FORKLIFT LEAVING");
            }
            forkliftStartTime = millis();       }
    }
}

//=========

void moveForklift() {
        // flashing led simulates forklift entering or leaving
    if (forkliftState == ENTERING or forkliftState == LEAVING) {
        forkliftMovingFlash();
        if (millis() - forkliftStartTime >= forkliftMovePeriod) {
            if (forkliftState == ENTERING) {
                forkliftState = LOADING;
                loadStartTime = millis();
                digitalWrite(forkliftLedPin, HIGH);
                Serial.println("FORKLIFT LOADING");
            }
            else {
                forkliftState = STOPPED;
                digitalWrite(forkliftLedPin, LOW);
                Serial.println("FORKLIFT STOPPED");
            }
        }
    }
}

//=========

void loadForklift() {
    if (forkliftState == LOADING) {
        loadingFlash();
        if (millis() - loadStartTime >= loadingPeriod) {
            forkliftState = READY_TO_LEAVE;
            digitalWrite(loadingLedPin, LOW);
            forkliftStartTime = millis();
            Serial.println("FORKLIFT READY TO LEAVE");
        }
    }
}

//=========

void forkliftMovingFlash() {
    static unsigned long prevFlashTime = 0;
    static unsigned long flashPeriod = 200;
    if (millis() - prevFlashTime >= flashPeriod) {
        prevFlashTime = millis();
        if (digitalRead(forkliftLedPin) == LOW)  {
            digitalWrite(forkliftLedPin, HIGH);
        }
        else {
            digitalWrite(forkliftLedPin, LOW);
        }
    }
}

//=========

void loadingFlash() {
        // simulates forklift picking up load
    static unsigned long prevFlashTime = 0;
    static unsigned long flashPeriod = 100;
    if (millis() - prevFlashTime >= flashPeriod) {
        prevFlashTime = millis();
        if (digitalRead(loadingLedPin) == LOW) {
            digitalWrite(loadingLedPin, HIGH);
        }
        else {
            digitalWrite(loadingLedPin, LOW);
        }
    }
}

//=========

bool updateDoorServo(char direction) {
    static unsigned long prevMoveTime = 0;
    static unsigned long moveInterval = 30;
    static int servoPos = servoMin;
    static int servoStep = 10;
    bool moveDone = false;

    if (millis() - prevMoveTime >= moveInterval) {
        prevMoveTime = millis();
        if (direction == 'O') {
            if (servoPos < servoMax) {
                servoPos += servoStep;
            }
            else {
                moveDone = true;
            }
        }

        if (direction == 'C') {
            if (servoPos > servoMin) {
                servoPos -= servoStep;
            }
            else {
                moveDone = true;
            }
        }
        doorServo.writeMicroseconds(servoPos);
    }

    return moveDone;
}

More detailed overview of the program

The principal functions in the code are as follows…
checkButton();
checkMayDoorMove();
operateDoor();
checkMayForkliftMove();
moveForklift();
loadForklift();

checkButton() checks the button pin but it does nothing else unless the doorState is LOCKED and the forkliftState is STOPPED. Then it changes doorState to UNLOCKED.

checkMayDoorMove() does nothing unless the forkliftState is STOPPED. Then it checks if doorState in UNLOCKED or OPEN and changes it to either OPENING or CLOSING.

operateDoor() only operates if doorState is OPENING or CLOSING. Note how it need not concern itself with what the forklift is doing because that was already checked in checkMayDoorMove(). When the movement is complete it changes doorState to either OPEN or LOCKED. The function updateDoorServo() is what actually moves the servo.

checkMayForkliftMove() only applies if forkliftState is STOPPED or READY_TO_LEAVE. Then it checks that the doorState is OPEN and changes the forkliftState to either ENTERING or LEAVING. In both cases it also starts the timer for the forklift move by saving the value of millis() into the variable forkliftStartTime.

Note that it is not sufficient just to check if the doorState is OPEN. By checking for forkliftState STOPPED or READY_TO_LEAVE the change the function makes to forkliftState ensures that checkMayForkliftMove() will do nothing next time it is called. Without that the code will probably get into an endless loop. This business of changing the state to make the program move on is a general feature of the functions in a State Machine program.

moveForklift() applies if forkliftState is ENTERING or LEAVING. For the purposes of this demo it is assumed that it takes few seconds for the forklift to drive in or out of the warehouse and an LED flashes while our theoretical forklift is moving. When the time is up (i.e. when the drive in or out is complete) forkliftState changes to LOADING or STOPPED. In the case where the state changes to LOADING the function also saves the value of millis() to the variable loadStartTime to facilitate timing the loading action.

loadForklift() only applies if forkliftState is LOADING. For the purposes of this demo this is also simulated by time passing and an LED flashing. Then the loading is complete it changes forkliftState to READY_TO_LEAVE and saves the value of millis() to forkliftStartTime in order to time the drive out of the warehouse.

Things to note

Because of the way the code is written in loop() each of the functions gets called repeatedly but only the function appropriate for the latest state of the system will respond - the others will do nothing.

When a function has finished its action it changes the state to something else. For example if the doorState is OPENING the function operateDoor() will move the servo and when it gets to the end of the movement it will change doorState to OPEN. That change in the state means that that the operateDoor() function will become inactive. This is a simple mechanism for causing things to stop even though other parts of the program remain active - or the activity moves to a different part of the program.

In many ways what I have described and programmed is fairly straightforward and you could probably get the same effect with code that does not use the state-machine concept. However it is my view that using the State Machine concept greatly simplifies the definition of a complex project and the design, development and debugging of the program.

In Reply #XX the concept is extended to allow the START button to also be used to cause a RESET while the action is in progress.

A few words about ENUMs

In the program I am using ENUMs to represent the states. I hope the way I have presented them in the code makes it easy to follow and adapt for your needs. An ENUM is just a simple way to create a series of numbers with meaningful names. When writing the program you don’t need to know the numbers - just focus on the names. That makes it very simple if you find you need to add additional states.

If you really do want to see the numeric value you can print it with code like this

Serial.println(doorStateENUM::OPEN);
// OR
doorState = OPENING;
Serial.println(doorState);

Unfortunately I have not been able to find a link to a simple introductory tutorial for enums.

Adding a Reset button

Let’s now extend the first program a little by adding a RESET button so that if it is pressed while the action is underway the forklift will immediately come out of the warehouse and the door will close and lock. When the RESET button is pressed the system could be in any state such as starting to open the doors, or the forklift could be inside and loading or leaving. All those situations (and any others) need to be catered for.

In the extended version of the program I have added a third ENUM called systemState which can have the values NORMAL, RESET_REQUESTED and RESET_IN_PROGRESS. I have extended checkButton() and added a new function checkResetRequest() to deal with this.

Note how, because of the way the existing states flow together the checkResetRequest() function only needs to check for two situations - the door OPENING (which can only happen if the forklift is STOPPED) and the forklift not STOPPED (which implies that the door is OPEN). (For simplicity I have just a couple of lines of code to instantly stop a loading that may be in progress)

Note also how, by changing the doorState or the forkliftState the checkResetRequest() function can set in motion all of the steps necessary to get the forklift back to the STOPPED position and the door LOCKED.

And when the forklift is STOPPED and the door is LOCKED checkResetRequest() sets the systemState back to NORMAL

The extended program

#include <Servo.h>

enum doorStateENUM {
    LOCKED,
    UNLOCKED,
    OPENING,
    OPEN,
    CLOSING,
};

doorStateENUM doorState;

enum forkliftStateENUM {
    STOPPED,
    ENTERING,
    LOADING,
    READY_TO_LEAVE,
    LEAVING,

};

forkliftStateENUM forkliftState;

enum systemStateENUM {
    NORMAL,
    RESET_REQUESTED,
    RESET_IN_PROGRESS,
};

systemStateENUM systemState;

Servo doorServo;

unsigned long forkliftStartTime;
unsigned long loadStartTime;
unsigned long forkliftMovePeriod = 2000;
unsigned long loadingPeriod = 2000;

byte startButtonPin = 9;
byte forkliftLedPin = 10;
byte loadingLedPin = 7;
byte doorServoPin = 8;
byte resetButtonPin = 5;

int servoMin = 1000;
int servoMax = 2000;

//=========

void setup() {
    Serial.begin(115200);
    Serial.println("Starting StateMachineDemo");

    pinMode(startButtonPin, INPUT_PULLUP);
    pinMode(resetButtonPin, INPUT_PULLUP);
    pinMode(forkliftLedPin, OUTPUT);
    pinMode(loadingLedPin, OUTPUT);
    digitalWrite(forkliftLedPin, LOW);
    digitalWrite(loadingLedPin, LOW);
    doorServo.attach(doorServoPin);
    doorServo.writeMicroseconds(servoMin);

    systemState = NORMAL;
    Serial.println("SYSTEM NORMAL");

    doorState = LOCKED;
    Serial.println("DOOR LOCKED");
    forkliftState = STOPPED;
    Serial.println("FORKLIFT STOPPED");

}

//=========

void loop() {
    checkButtons();
    checkMayDoorMove();
    operateDoor();
    checkMayForkliftMove();
    moveForklift();
    loadForklift();
    checkResetRequest();
    setSystemNormal();
}

//=========

void checkButtons() {
    byte startButtonState = digitalRead(startButtonPin);
    byte resetButtonState = digitalRead(resetButtonPin);
    if (systemState == NORMAL) {
        if (doorState == LOCKED and forkliftState == STOPPED) { // the action has not started
            if (startButtonState == LOW) {
                doorState = UNLOCKED;
                forkliftStartTime = millis();
                Serial.println("DOOR UNLOCKED");
            }
        }
        if (resetButtonState == LOW) { // the action is underway
            systemState = RESET_REQUESTED;
            Serial.println("SYSTEM RESET REQUESTED");
        }
    }

}

//=========

void checkMayDoorMove() {
    if (forkliftState == STOPPED) {
        if (doorState == UNLOCKED) {
            doorState = OPENING;
            Serial.println("DOOR OPENING");
        }
        if (doorState == OPEN) {
            doorState = CLOSING;
            Serial.println("DOOR CLOSING");
        }
    }
}

//=========

void operateDoor() {
        // servo simulates door opening
    if (doorState == OPENING or doorState == CLOSING) {
        bool moveComplete = false;
        if (doorState == OPENING) {
            moveComplete = updateDoorServo('O');
            if (moveComplete == true) {
                doorState = OPEN;
                Serial.println("DOOR OPEN");
            }
        }
        if (doorState == CLOSING) {
            moveComplete = updateDoorServo('C');
            if (moveComplete == true) {
                doorState = LOCKED;
                Serial.println("DOOR LOCKED");
            }
        }
    }
}

//========

void checkMayForkliftMove() {
    if (forkliftState == STOPPED or forkliftState == READY_TO_LEAVE) {
        if (doorState == OPEN) {
            if (forkliftState ==  STOPPED) {
                forkliftState = ENTERING;
                Serial.println("FORKLIFT ENTERING");
            }
            if (forkliftState == READY_TO_LEAVE) {
                forkliftState = LEAVING;
                Serial.println("FORKLIFT LEAVING");
            }
            forkliftStartTime = millis();
        }
    }
}

//=========

void moveForklift() {
        // flashing led simulates forklift entering or leaving
    if (forkliftState == ENTERING or forkliftState == LEAVING) {
        forkliftLedFlash();
        if (millis() - forkliftStartTime >= forkliftMovePeriod) {
            if (forkliftState == ENTERING) {
                forkliftState = LOADING;
                loadStartTime = millis();
                digitalWrite(forkliftLedPin, HIGH);
                Serial.println("FORKLIFT LOADING");
            }
            else {
                forkliftState = STOPPED;
                digitalWrite(forkliftLedPin, LOW);
                Serial.println("FORKLIFT STOPPED");
            }
        }
    }
}

//=========

void loadForklift() {
    if (forkliftState == LOADING) {
        loadingFlash();
        if (millis() - loadStartTime >= loadingPeriod) {
            forkliftState = READY_TO_LEAVE;
            digitalWrite(loadingLedPin, LOW);
            forkliftStartTime = millis();
            Serial.println("FORKLIFT READY TO LEAVE");
        }
    }
}

//=========

void forkliftLedFlash() {
    static unsigned long prevFlashTime = 0;
    static unsigned long flashPeriod = 200;
    if (millis() - prevFlashTime >= flashPeriod) {
        prevFlashTime = millis();
        if (digitalRead(forkliftLedPin) == LOW) {
            digitalWrite(forkliftLedPin, HIGH);
        }
        else {
            digitalWrite(forkliftLedPin, LOW);
        }
    }
}

//=========

void loadingFlash() {
        // simulates forklift picking up load
    static unsigned long prevFlashTime = 0;
    static unsigned long flashPeriod = 100;
    if (millis() - prevFlashTime >= flashPeriod) {
        prevFlashTime = millis();
        if (digitalRead(loadingLedPin) == LOW) {
            digitalWrite(loadingLedPin, HIGH);
        }
        else {
            digitalWrite(loadingLedPin, LOW);
        }
    }
}

//=========

bool updateDoorServo(char direction) {
    static unsigned long prevMoveTime = 0;
    static unsigned long moveInterval = 30;
    static int servoPos = servoMin;
    static int servoStep = 10;
    bool moveDone = false;


    if (millis() - prevMoveTime >= moveInterval) {
        prevMoveTime = millis();
        if (direction == 'O') {
            if (servoPos < servoMax) {
                servoPos += servoStep;
            }
            else {
                moveDone = true;
            }
        }

        if (direction == 'C') {
            if (servoPos > servoMin) {
                servoPos -= servoStep;
            }
            else {
                moveDone = true;
            }
        }
        doorServo.writeMicroseconds(servoPos);
    }

    return moveDone;
}

//==========

void checkResetRequest() {
    if (systemState == RESET_REQUESTED) {
            systemState = RESET_IN_PROGRESS;

            // now deal with the possible states the door and the forklift might in

            // door may be opening so close it again
        if (doorState == OPENING) {
            doorState = CLOSING;
            Serial.println("DOOR CLOSING");
        }

            // if the forklift is not stopped make it leave the warehouse
            // this will automatically be followed by the door closing
        if (forkliftState != STOPPED) {
            if (forkliftState == LOADING) {
                digitalWrite(loadingLedPin, LOW); // stop loading
            }
            forkliftState = LEAVING;
            forkliftStartTime = millis();
            Serial.println("FORKLIFT LEAVING");
        }
    }
}

//===========

void setSystemNormal() {
    if (systemState == RESET_IN_PROGRESS) {
        if (doorState == LOCKED and forkliftState == STOPPED) {
            systemState = NORMAL;
            Serial.println("SYSTEM NORMAL");
        }
    }
}

…End of Tutorial

…R

Reserved for future use

...R

You might want to say something about how, although state machines are a simple concept, people tend to struggle to grasp the idea initially. Examples really help. To that end, perhaps some less complex machines would make an easier lead in to your warehouse code.

wildbill:
Examples really help. To that end, perhaps some less complex machines would make an easier lead in to your warehouse code.

I am open to suggestions - what have you in mind?

...R

Perhaps it would benefit from a state diagram, along the lines of this one that was posted earlier today in the context of button sensing:

source

I suggest trying to use standard peripherals like LED_BUILTIN if you can. Not everyone has a servo. This example uses only an input pin with input pullup so a jumper wire will trigger it, and the onboard LED. It's also as simple as it can be.

/*
  Digital Input Pullup State Machine Example
  Ken Willmott 2020

  This example demonstrates the use of a state machine. It reads a digital
  input on pin 2 and toggles the LED between on, off, and blinking.

  The circuit:
  - momentary switch attached from pin 2 to ground
  - built-in LED on pin 13 or whatever pin is assigned to LED_BUILTIN

  Unlike pinMode(INPUT), there is no pull-down resistor necessary. An internal
  20K-ohm resistor is pulled to 5V. This configuration causes the input to read
  HIGH when the switch is open, and LOW when it is closed.
*/

const int ledPin =  LED_BUILTIN;// the number of the LED pin
const int buttonPin = 2;
const int interval = 500;
byte lastButtonState;
unsigned long previousMillis = 0;        // will store last time LED was updated

enum LEDStateENUM {
  OFF,
  ON,
  BLINKING
};

LEDStateENUM LEDState = OFF;

void setup() {
  //configure pin 2 as an input and enable the internal pull-up resistor
  pinMode(buttonPin, INPUT_PULLUP);
  //configure LED output
  pinMode(ledPin, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();

  // read the pushbutton input pin:
  byte buttonState = digitalRead(buttonPin);

  // compare the buttonState to its previous state
  if (buttonState != lastButtonState) {
    // if the button state has changed, transition to the next LED state:
    if (buttonState == LOW) {
      switch (LEDState)
      {
        case OFF:
          LEDState = ON;
          break;

        case ON:
          LEDState = BLINKING;
          break;

        case BLINKING:
          LEDState = OFF;
          break;
      }
    }
    // Delay a little bit to avoid bouncing
    delay(50);
  }
  // save the current state as the last state, for next time through the loop
  lastButtonState = buttonState;


  // Now perform the LED update:
  //
  switch (LEDState)
  {
    case OFF:
      digitalWrite(ledPin, LOW);
      break;

    case ON:
      digitalWrite(ledPin, HIGH);
      break;

    case BLINKING:
      if (currentMillis - previousMillis >= interval) {
        // save the last time you blinked the LED
        previousMillis = currentMillis;

        // if the LED is off turn it on and vice-versa:
        digitalWrite(ledPin, not digitalRead(ledPin) );
      }
      break;
  }
}

I like tutorials that show their work, including backtracking or refactoring. It helps with understanding to see how the program evolved.

To that end, I suggest a home heating system. Two states initially, controlled by a temperature sensor.

Then add an on/off switch. Perhaps a cooling system too. Add additional complexity such that the heater must take a five minute break when its done with the current cycle. Fan must run for two minutes after heating stops. Etc.etc.

I've noticed that even when someone finally says that they've got it, they will still fail when asked to add functionality to an existing state machine so l'm hoping that showing how the system came to be will help with that.

Kudos to you for taking this on.

Robin I think you're on the right track. I agree with blomcrestlight that an introduction to state machines using a state diagram would be very helpful in instilling a conceptual grasp of the idea. Indeed I would start with a simple state diagram with 2 states and 1 or 2 events that would solidify the idea. Then morph this generic state diagram to a very simple real world example of a single button toggling between 2 states. Now you could expand that to the door/forklift example.

Once people see how a simple state diagram is translated into code then they would have a much easier time following a more complex implementation.

blomcrestlight:
Perhaps it would benefit from a state diagram, along the lines of this one that was posted earlier today

I confess I am not a great fan of those diagrams. I don't think they are easy for a newbie to assimilate.

I think the concept of a state diagram would need a tutorial all of its own. Maybe you would consider writing one that would dovetail with my tutorial?

...R

ToddL1962:
Indeed I would start with a simple state diagram with 2 states and 1 or 2 events that would solidify the idea. Then morph this generic state diagram to a very simple real world example of a single button toggling between 2 states.

If you would care to write up that suggestion I will certainly take it into account.

There are lots of ways to code the idea of a button toggling between 2 states. IMHO it would not be sufficient to enthuse somebody to learn about State Machines.

...R

I strongly agree a state diagram is the first concept that needs to be grasped. I also think the structure of the example code inherently limits the complexity of the state machine that can be produced.

I would MUCH prefer to see a switch statement used to process the states, rather than conditionals in all those functions. Once you get past a small number of states, the method shown here will become unmanageable. Using a switch statement, I’ve produced state machines with hundreds of states, and the ALL of the functionality for each state is typically no more a page of very simple code, all in one place, rather than being spread over dozens of separate functions. The switch structure also makes it trivial to add special processing like code that gets execute ONLY on the first execution of a particular state, delays within or between states, or timeouts on states, and even explicit error handling, operation (or sequence) retries. I’ve even implemented sequence functions, with nested call and return capability. With more complex machines (e.g. those doing motion or process control), these capabilities are critical, and I don’t see how they could reasonably be handled in the given structure.

Regards,
Ray L.

Robin2:
Maybe you would consider writing one that would dovetail with my tutorial?

Willco; never done one with 2 before (the door and the vehicle here), but always happy to learn.

RayLivingston:
I would MUCH prefer to see a switch statement used to process the states, rather than conditionals in all those functions.

I'd agree with that, not that I've ever done one with 100s of states, but can see the "if" approach getting cumbersome vs "switch..case".

This one introduces another state for the button debounce so there is no delay() anywhere:

/*
  Digital Input Pullup State Machine

  This example demonstrates the use of a state machine. It reads a digital
  input on pin 2 and toggles the LED between on, off, and blinking.

  The circuit:
  - momentary switch attached from pin 2 to ground
  - built-in LED on pin 13 or whatever pin is assigned to LED_BUILTIN

  Unlike pinMode(INPUT), there is no pull-down resistor necessary. An internal
  20K-ohm resistor is pulled to 5V. This configuration causes the input to read
  HIGH when the switch is open, and LOW when it is closed.
*/

const int ledPin =  LED_BUILTIN;// the number of the LED pin
const int buttonPin = 2;
const int interval = 500;
byte lastButtonState;
unsigned long previousLEDMillis = 0;        // will store last time LED was updated
unsigned long previousDebounceMillis = 0;        // will store last time key change

enum LEDStateENUM {
  OFF,
  ON,
  BLINKING
};
LEDStateENUM LEDState = OFF;

enum debounceStateENUM {
  DELAY,
  READ
};
debounceStateENUM debounceState = READ;

void setup() {
  //configure pin 2 as an input and enable the internal pull-up resistor
  pinMode(buttonPin, INPUT_PULLUP);
  //configure LED output
  pinMode(ledPin, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();

  // read the pushbutton input pin:
  byte buttonState = digitalRead(buttonPin);

  if (debounceState == READ)
  {
    // compare the buttonState to its previous state
    if (buttonState != lastButtonState) {
      previousDebounceMillis = currentMillis;
      lastButtonState = buttonState;
      debounceState = DELAY;
      
      // if the button state has changed, transition to the next LED state:
      if (buttonState == LOW) {
        switch (LEDState)
        {
          case OFF:
            LEDState = ON;
            break;

          case ON:
            LEDState = BLINKING;
            break;

          case BLINKING:
            LEDState = OFF;
            break;
        }
      }
    }
  }
  else if (debounceState == DELAY)
  {
    // Delay a little bit to avoid bouncing
    if (currentMillis - previousDebounceMillis >= 50) {
      // delay is complete - go back to reading
      debounceState = READ;
    }
  }

  // Now perform the LED update:
  //
  switch (LEDState)
  {
    case OFF:
      digitalWrite(ledPin, LOW);
      break;

    case ON:
      digitalWrite(ledPin, HIGH);
      break;

    case BLINKING:
      if (currentMillis - previousLEDMillis >= interval) {
        // save the last time you blinked the LED
        previousLEDMillis = currentMillis;

        // if the LED is off turn it on and vice-versa:
        digitalWrite(ledPin, not digitalRead(ledPin) );
      }
      break;
  }
}

This is also a minimal demonstration of multitasking, which is the whole point of using states. It demonstrates state handling with both if-else and switch constructs.

Robin2:
If you would care to write up that suggestion I will certainly take it into account.

There are lots of ways to code the idea of a button toggling between 2 states. IMHO it would not be sufficient to enthuse somebody to learn about State Machines.

...R

I will be glad to when I have the opportunity.

I agree a button toggling between two states is not compelling. The point would be to illustrate the mechanics of implementing that simple state machine in code before moving to the more complex state machine.

Arduino Forum aims at educating the novices in microcontroller hardware and C/C++ based programming using Arduino UNO Learning Kit and Arduino IDE. Therefore, an innovative effort may begin with a basic project and then gradually moving to complex under the umbrella of these two functions of Arduino IDE: setup() and loop() and then users functions may come in as needed. I see something like this: // python-build-start -- what does python do here?

There are many ways of formulating the solution of a problem before it is coded into a particular high level programming language. Flow Chart is a powerful tool, which can describe the solution of a problem of any complexity level in visual perspective through the interconnections of few pre-defined geometrical figures. For example: the programming task (just an exercise) of 'activating relays based on button pressed (Fig-1)' may begin with the following Flow Chart (Fig-2) before it is coded into Arduino/C Language. During coding process, all the variables and functions will come as needed.

flow-2a.png
Figure-1:


Figure-2:

flow-2a.png

RayLivingston:
Using a switch statement, I've produced state machines with hundreds of states, and the ALL of the functionality for each state is typically no more a page of very simple code, all in one place, rather than being spread over dozens of separate functions.

I don't follow that.

If each state requires a page of code and there are hundreds of state doesn't that mean that the function in which the SWITCH takes place runs to hundreds (or at least dozens) of pages?

Perhaps you can post a link to an example.

...R

ToddL1962:
I will be glad to when I have the opportunity.

Might that be within the next few days?

...R