Hello everybody,
hello especially "almost" newbees,
I would like to read your opinion about the understandability of the program below. It is a program that demonstrates the use of a programming-technique called "state-machine"
The focus in this program is set on make it easy to follow the program-FLOW.
Though as an "expert" you become compulsorily blind about beginners difficulties. So it might be that there are some points that still rise questions.
I see this as "add another variant to explain state-machines as different people have different approaches in learning something new".
Everybody is welcomed to comment on the code. Especially the newbees.
you can find the code in the last posting posted by me
In this post you find the initioal code-version.
As this code is INTENDED to evolve through feedback
the most newest code-version is in the last posting postd by me
So here is the initial code-version 0.1:
// dear newbee,
// this program demonstrates how "state-machines" work
// at the bottom of this file there is an explanation you should read first
const byte ButtonPin = 12;
const byte OnBoard_LED = 13;
// constants for the different states
const byte WaitForButtonPressed = 1;
const byte SwitchON_LED = 2;
const byte WaitUntil_LED_ON_PeriodIsOver = 3;
const byte SwitchOFF_LED = 4;
const byte WaitUntil_LED_OFF_PeriodIsOver = 5;
byte MyStateVar;
// variables for non-blocking-timing
unsigned long currentMillis;
unsigned long LED_ON_PeriodStart;
unsigned long LED_OFF_PeriodStart;
unsigned long TimeDifferencePassedBy;
// variables for timeperiods
unsigned long LED_ON_Period = 6000;
unsigned long LED_OFF_Period = 10000;
long CountLoops = 0;
#define pressed LOW
void setup() {
Serial.begin(115200);
Serial.println("Setup-Start");
PrintFileNameDateTime();
pinMode(ButtonPin, INPUT_PULLUP);
digitalWrite(OnBoard_LED, LOW);
pinMode(OnBoard_LED, OUTPUT);
MyStateVar = WaitForButtonPressed;
Serial.println("press button at least for two seconds to start the demo");
}
void loop() {
CountLoops++;
slowDownSerialOutPut();
PrintStateInfo();
currentMillis = millis();
// this is the state-machine based on the switch-case-statement
// depending on the value of variable "MyStateVar" only the commands
// below one "case" gets executed
switch (MyStateVar) {
case WaitForButtonPressed:
// a state that waits for an external input to occur
if (digitalRead(ButtonPin) == pressed) {
MyStateVar = SwitchON_LED;
Serial.println("button is pressed");
}
break;
case SwitchON_LED:
// a state that does an action just ONE time
// because the state-variable is set immediately to the next state
Serial.println("I switch on the LED start non-blocking-timing..");
digitalWrite(OnBoard_LED, HIGH);
LED_ON_PeriodStart = currentMillis;
MyStateVar = WaitUntil_LED_ON_PeriodIsOver;
break;
case WaitUntil_LED_ON_PeriodIsOver:
Serial.print("Keep LED On some time. Time passed by since LED switched on ");
Serial.print(currentMillis - LED_ON_PeriodStart);
Serial.println(" milliseconds");
// a state that waits for a timeperiod to pass by
if (currentMillis - LED_ON_PeriodStart >= LED_ON_Period) {
Serial.println("Wait-Time with LED ON is over)");
MyStateVar = SwitchOFF_LED;
}
break;
case SwitchOFF_LED:
// a state that does an action just ONE time
// because the state-variable is set immetiately to the next state
Serial.println("I switch OFF LED. Start non-blocking timing...");
digitalWrite(OnBoard_LED, LOW);
LED_OFF_PeriodStart = currentMillis;
MyStateVar = WaitUntil_LED_OFF_PeriodIsOver;
break;
case WaitUntil_LED_OFF_PeriodIsOver:
// a state that waits for a timeperiod to pass by
Serial.print("Wait some time. Time passed by since LED switched on ");
Serial.print(currentMillis - LED_OFF_PeriodStart);
Serial.println(" milliseconds");
if (currentMillis - LED_OFF_PeriodStart >= LED_OFF_Period) {
Serial.println("Wait-Time with LED OFF is over");
Serial.println("I'm ready for a new buttonpress");
MyStateVar = WaitForButtonPressed;
}
break;
}
}
// below here are "helper-functions" for the visualisation
// of what the program-FLOW does
// slowing down the speed of the program to reduce the number of lines
// printed to the serial monitor
// in real applications this slowing down is just not needed
void slowDownSerialOutPut() {
delay(1000);
}
void PrintStateInfo() {
Serial.println();
Serial.print("CountLoops ");
Serial.print(CountLoops);
Serial.print(" I'm in state ");
Serial.print(MyStateVar);
PrintActualStateText(MyStateVar);
Serial.println();
}
void PrintActualStateText(byte p_state) {
if (p_state == WaitForButtonPressed) {
Serial.print(" Wait for Button-Press");
}
if (p_state == SwitchON_LED) {
Serial.print(" Switch ON LED ");
}
if (p_state == WaitUntil_LED_ON_PeriodIsOver) {
Serial.print(" Wait until LED-ON-Time has passed by ");
}
if (p_state == SwitchOFF_LED) {
Serial.print(" Switch OFF LED ");
}
if (p_state == WaitUntil_LED_OFF_PeriodIsOver) {
Serial.print(" Wait until some is time is over");
}
}
// to be able to determine what exactly sourcecode is running
// I add this function to all my programs
// the content is updated EVERY time you compile new (at least the time-stamp)
void PrintFileNameDateTime() {
Serial.println("Code running comes from file ");
Serial.println(__FILE__);
Serial.print(" compiled ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.println(__TIME__);
}
// explanation:
// dear newbee,
// the program expects a button connected to IO-pin 12
// and does switch on / off IO-pin 13 which is connected to the onboard-LED
// on an arduino uno-board
// a state-machine is a medium advanced programming-technique.
// So if you hardly know what a constant, a variable, an if-condition is
// understanding this program can be ambitious and hard
// in this case I recommend learning the fundamental basics first
// I'm very interested in your experience if this program is easy to understand
// or hard to understand
// so whatever questions you have about this code your questions from YOU are a
// very welcomed feedback to improve the understandability
// the main code has around 100 lines. So indeed it will take some time to understand it.
// for watching what the program-FLOW does start the serial monitor and adjust baud to
// 115200 baud
// after 20 seconds deactivate "autoscroll" in the serial monitor or unplug your microcontroller-board
// and then scroll back the serial monitor and start reading the lines
// to understand what the program does.
// again: If you have any questions feel free to ask the questions in the Arduino-Forum
// I'm very interested in your questions because your questions are feedback what is still
// difficult to understand.
I would also recommend you adopt camelCase as this is what Arduino is using all over the place and use const or constexpr instead of #define to benefit from type checking (a good habit to give newbies).
I would put the state machine in a function of its own so that the loop would look "clean and lean"
I would put all the ancillary functions before setup() and loop() and in order of dependencies as this is what C++ expects (you rely on the IDE auto-generating functions prototypes for you to pre-declare stuff - which will come bite newbies at some point)
If this is of any interest, I wrote a tutorial on state machines' basics in French that has been helpful I was told (I assume GoogleTranslate can help). So If you were to write an English tutorial I would suggest to introduce concepts in text rather than just dump a piece of code.
Hope this helps, indeed it's a common design pattern newbies should learn to master.
thank you very much for your feedback.
enumeration is professional but adds another new thing to learn
So here is version 0.2 based on your feedback
// dear newbee,
// this program demonstrates how "state-machines" work
// at the bottom of this file there is an explanation you should read first
const byte buttonPin = 12;
const byte onBoard_LED = 13;
// constants for the different states
const byte waitForButtonPressed = 1;
const byte switchON_LED = 2;
const byte waitUntil_LED_ON_PeriodIsOver = 3;
const byte switchOFF_LED = 4;
const byte waitUntil_LED_OFF_PeriodIsOver = 5;
byte myStateVar;
// variables for non-blocking-timing
unsigned long currentMillis;
unsigned long LED_ON_PeriodStart;
unsigned long LED_OFF_PeriodStart;
unsigned long TimeDifferencePassedBy;
// variables for timeperiods
unsigned long LED_ON_Period = 6000;
unsigned long LED_OFF_Period = 10000;
long countLoops = 0;
#define pressed LOW
// If you are interested in the main-code scroll down to
// void setup()
// below here are "helper-functions" for the visualisation
// of what the program-FLOW does
// slowing down the speed of the program to reduce the number of lines
// printed to the serial monitor
// in real applications this slowing down is just not needed
void slowDownSerialOutPut() {
delay(1000);
}
void printActualStateText(byte p_state) {
if (p_state == waitForButtonPressed) {
Serial.print(" Wait for Button-Press");
}
if (p_state == switchON_LED) {
Serial.print(" Switch ON LED ");
}
if (p_state == waitUntil_LED_ON_PeriodIsOver) {
Serial.print(" Wait until LED-ON-Time has passed by ");
}
if (p_state == switchOFF_LED) {
Serial.print(" Switch OFF LED ");
}
if (p_state == waitUntil_LED_OFF_PeriodIsOver) {
Serial.print(" Wait until some is time is over");
}
}
void printStateInfo() {
Serial.println();
Serial.print("CountLoops ");
Serial.print(countLoops);
Serial.print(" I'm in state ");
Serial.print(myStateVar);
printActualStateText(myStateVar);
Serial.println();
}
// to be able to determine which exactly sourcecode-vrsion is running
// I add this function to all my programs
// the content is updated EVERY time you compile new (at least the time-stamp)
void PrintFileNameDateTime() {
Serial.println("Code running comes from file ");
Serial.println(__FILE__);
Serial.print(" compiled ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.println(__TIME__);
}
void manageStates() {
currentMillis = millis();
// this is the state-machine based on the switch-case-statement
// depending on the value of variable "MyStateVar" only the commands
// below one "case" gets executed
switch (myStateVar) {
case waitForButtonPressed:
// a state that waits for an external input to occur
if (digitalRead(buttonPin) == pressed) {
myStateVar = switchON_LED;
Serial.println("button is pressed");
}
break;
case switchON_LED:
// a state that does an action just ONE time
// because the state-variable is set immediately to the next state
Serial.println("I switch on the LED start non-blocking-timing..");
digitalWrite(onBoard_LED, HIGH);
LED_ON_PeriodStart = currentMillis;
myStateVar = waitUntil_LED_ON_PeriodIsOver;
break;
case waitUntil_LED_ON_PeriodIsOver:
Serial.print("Keep LED On some time. Time passed by since LED switched on ");
Serial.print(currentMillis - LED_ON_PeriodStart);
Serial.println(" milliseconds");
// a state that waits for a timeperiod to pass by
if (currentMillis - LED_ON_PeriodStart >= LED_ON_Period) {
Serial.println("Wait-Time with LED ON is over)");
myStateVar = switchOFF_LED;
}
break;
case switchOFF_LED:
// a state that does an action just ONE time
// because the state-variable is set immetiately to the next state
Serial.println("I switch OFF LED. Start non-blocking timing...");
digitalWrite(onBoard_LED, LOW);
LED_OFF_PeriodStart = currentMillis;
myStateVar = waitUntil_LED_OFF_PeriodIsOver;
break;
case waitUntil_LED_OFF_PeriodIsOver:
// a state that waits for a timeperiod to pass by
Serial.print("Wait some time. Time passed by since LED switched on ");
Serial.print(currentMillis - LED_OFF_PeriodStart);
Serial.println(" milliseconds");
if (currentMillis - LED_OFF_PeriodStart >= LED_OFF_Period) {
Serial.println("Wait-Time with LED OFF is over");
Serial.println("I'm ready for a new buttonpress");
myStateVar = waitForButtonPressed;
}
break;
}
} // end of function manageStates()
void setup() {
Serial.begin(115200);
Serial.println("Setup-Start");
PrintFileNameDateTime();
pinMode(buttonPin, INPUT_PULLUP);
digitalWrite(onBoard_LED, LOW);
pinMode(onBoard_LED, OUTPUT);
myStateVar = waitForButtonPressed;
Serial.println("press button at least for two seconds to start the demo");
}
// parts of the program that belong to one functional unit are coded in their
// own function where each function has a self-explaining name
// This makes it much easier to test code and keep an overview
// with the growing of the code
void loop() {
countLoops++;
slowDownSerialOutPut();
printStateInfo();
manageStates();
}
// explanation:
// dear newbee,
// the program expects a button connected to IO-pin 12
// and does switch on / off IO-pin 13 which is connected to the onboard-LED
// on an arduino uno-board
// a state-machine is a medium advanced programming-technique.
// So if you hardly know what a constant, a variable, an if-condition is
// understanding this program can be ambitious and hard
// in this case I recommend learning the fundamental basics first
// I'm very interested in your experience if this program is easy to understand
// or hard to understand
// so whatever questions you have about this code your questions from YOU are a
// very welcomed feedback to improve the understandability
// the main code has around 100 lines. So indeed it will take some time to understand it.
// for watching what the program-FLOW does start the serial monitor and adjust baud to
// 115200 baud
// after 20 seconds deactivate "autoscroll" in the serial monitor or unplug your microcontroller-board
// and then scroll back the serial monitor and start reading the lines
// to understand what the program does.
// again: If you have any questions feel free to ask the questions in the Arduino-Forum
// I'm very interested in your questions because your questions are feedback what is still
// difficult to understand.
You could use if- else if - else if - else.. in your printActualStateText()function or a switch/case
in the first switch/Case for the state I would move the state change to the last instruction for coherence with the other cases (and makes sense to exit the case with the new state)
switch (myStateVar) {
case waitForButtonPressed:
// a state that waits for an external input to occur
if (digitalRead(buttonPin) == pressed) {
Serial.println("button is pressed");
myStateVar = switchON_LED;
}
break;
same thing here: adds a new thing. My Opinion is good learning means focusing on one aspect. My program has already multiple things
Of course going on with learning is good. But not all at the same time. (by the way this is the main bad thing youtubers do that try to explain someting in a
"I'm just talking to you"-style.
Some more details came to their mind adding this adding that - confusing the newbees more and more)
I have never yet looked close into the F()-macro because it has so many possible parameters.
So what is the difference that the F()-macro does to the code?
Just save memory? Not relevant here to me.
I have never yet looked close into the F()-macro because it has so many possible parameters.
it does not... just a quoted text that ends up in flash memory
yes just memory - and it does not hurt to add it. I tend to believe it's good to show best practices in code you share. Newbies might not get it but will just do so in the future and at one point will get it
I get your point of overloading the newcomer with extra information that is not relevant to the point you are trying to make though. In my tutorial I chose to introduce enum and switch/case as two building blocks before describing state machines.
As I've said before, state machines are mysteriously hard to comprehend when you first encounter them. The most viable way to explain them I can think of is to use multiple examples - one just doesn't seem to cut it.
I'd also like some of those examples to be solving a simple but tangible real world problem so the student can easily understand the why of it: HVAC for example.
It would also be nice to grow the solution so that the student can see how a simple (perhaps trivial) FSM gets extended when the requirements change. I've noticed users here trying to edit an existing state machine solution and unexpectedly finding that their understanding of how they work wasn't sufficient.
I've wondered about doing this a few times, but I could never convince myself that people would want to walk through such a necessarily long treatise.
I don't see why that should be if explained properly.
The system will be in a particular state that requires a set of instructions to be executed repeatedly until something happens to cause a change of state, at which time the system starts to execute a different set of instructions repeatedly until something happens to cause a change of state and so on
A combination of sensibly named states, use of switch/case and properly formatted code make the program structure very easy to see. Switch/case is much better for this than if/else if in my experience
Repeating myself again - FSMs are like pointers. Trivially simple and easy to understand and use. But almost everyone struggles with them to start with.
And you're right, they shouldn't be a problem if they're explained well - probably. But I can't help feeling that computer science teachers can't be held accountable for all of the problem - the persistence of the same difficulties with these things does suggest that there's a mental hurdle to clear for both of these concepts. I'd love to read a paper on the phenomenon actually.
Word. I am so glad I don't have to learn this stuff.
I think @StefanL38 has made a valiant effort. As he, and I, have said afore, everyone has a learning style; these days, fortunately, a little poking around will probably turn up something a noob can get a grip on. Or several varying.
With FSMs. I think a less trivial but not overwhelming example should be very next if not first.
I also cannot fathom getting anywhere with state machines without some kind of flow chart or state diagram graphic. So I guess I lean a bit to the right side on this.
And I don't even get the goal of this machine? Turn on an LED with a button press, turn it off after a delay, then play dumb to button presses for another delay period, go back to waiting for a button press. This doesn't seem like a real world thing. True such a mechanism does control the rate at which you can fire off missiles in Spacewar, so.
Like many things, hard until easy. Like many things, something to arrive at on a unique pathway. Like many things, hard to make simple for educational purposes.
I just posted in another thread where one way to achieve the desired functionality is a state-machine
The TO gave this description
,> currently my program is supposed to be a process that is started by a button. An example of how it should work is
13.00 = button pressed, relay turns on for 2 minutes
13.02 = relay turns off, waits for 8 minutes
13.10 = relay turns on
13.12 = relay turns off
etc. etc.
my response:
one way of realising such a functionality is thinking of it as doing different things:
Thing1: wait for start-button to be pressed
Thing2: switch on relay
Thing3: wait two minutes
Thing4: switch off relay
Thing5: wait 8 minutes
Thing6: go on with doing Thing2 (switch on relay)
Whenever you do Thing1 nothing else than this single Thing1 has to be done
Whenever you do Thing2 nothing else than this single Thing2 has to be done
...
Whenever you do Thing5 nothing else than this single Thing5 has to be done
Such a functionality can be coded using the switch-case-statement.
The above description uses just very common words to give a picture of the functionality.
IMHO this is one of the "secrets" of a good explanation building a brigde that starts at well known and easy to understand words.
hm - well so - yet another aspect I have to think of about my own democode....
I would go with the 'enum' since it allows the compiler to warn you if your switch/case is missing any cases.
I also only use one "PeriodStart" for all of the timing periods since no two periods overlap.
// dear newbee,
// this program demonstrates how "state-machines" work
// at the bottom of this file there is an explanation you should read first
const byte buttonPin = 12;
const byte onBoard_LED = 13;
// constants for the different states
enum States // Assigns names to a set of numbers starting at 0.
{
waitForButtonPressed,
switchON_LED,
waitUntil_LED_ON_PeriodIsOver,
switchOFF_LED,
waitUntil_LED_OFF_PeriodIsOver
} myStateVar;
const char *StateNames[] = // Just for display
{
"waitForButtonPressed,"
"switchON_LED",
"waitUntil_LED_ON_PeriodIsOver",
"switchOFF_LED",
"waitUntil_LED_OFF_PeriodIsOver"
};
// variables for non-blocking-timing
unsigned long currentMillis;
unsigned long PeriodStart;
// variables for timeperiods
unsigned long LED_ON_Period = 6000;
unsigned long LED_OFF_Period = 10000;
long countLoops = 0;
#define pressed LOW
// If you are interested in the main-code scroll down to
// void setup()
// below here are "helper-functions" for the visualisation
// of what the program-FLOW does
// slowing down the speed of the program to reduce the number of lines
// printed to the serial monitor
// in real applications this slowing down is just not needed
void slowDownSerialOutPut()
{
delay(1000); // BAD PRACTICE. This makes the sketch unresponsive for 99% of the time.
}
void printActualStateText(States p_state)
{
Serial.print(StateNames[p_state]);
}
void printStateInfo()
{
Serial.println();
Serial.print("CountLoops ");
Serial.print(countLoops);
Serial.print(" I'm in state ");
Serial.print(myStateVar);
printActualStateText(myStateVar);
Serial.println();
}
// to be able to determine which exactly sourcecode-version is running
// I add this function to all my programs
// the content is updated EVERY time you compile new (at least the time-stamp)
void PrintFileNameDateTime()
{
Serial.println("Code running comes from file ");
Serial.println(__FILE__);
Serial.print(" compiled ");
Serial.print(__DATE__);
Serial.print(" ");
Serial.println(__TIME__);
}
void manageStates()
{
currentMillis = millis();
// this is the state-machine based on the switch-case-statement
// depending on the value of variable "MyStateVar" only the commands
// below one "case" gets executed
switch (myStateVar)
{
case waitForButtonPressed:
// a state that waits for an external input to occur
if (digitalRead(buttonPin) == pressed)
{
myStateVar = switchON_LED;
Serial.println("button is pressed");
}
break;
case switchON_LED:
// a state that does an action just ONE time
// because the state-variable is set immediately to the next state
Serial.println("I switch on the LED start non-blocking-timing..");
digitalWrite(onBoard_LED, HIGH);
PeriodStart = currentMillis;
myStateVar = waitUntil_LED_ON_PeriodIsOver;
break;
case waitUntil_LED_ON_PeriodIsOver:
Serial.print("Keep LED On some time. Time passed by since LED switched on ");
Serial.print(currentMillis - PeriodStart);
Serial.println(" milliseconds");
// a state that waits for a timeperiod to pass by
if (currentMillis - PeriodStart >= LED_ON_Period)
{
Serial.println("Wait-Time with LED ON is over)");
myStateVar = switchOFF_LED;
}
break;
case switchOFF_LED:
// a state that does an action just ONE time
// because the state-variable is set immetiately to the next state
Serial.println("I switch OFF LED. Start non-blocking timing...");
digitalWrite(onBoard_LED, LOW);
PeriodStart = currentMillis;
myStateVar = waitUntil_LED_OFF_PeriodIsOver;
break;
case waitUntil_LED_OFF_PeriodIsOver:
// a state that waits for a timeperiod to pass by
Serial.print("Wait some time. Time passed by since LED switched on ");
Serial.print(currentMillis - PeriodStart);
Serial.println(" milliseconds");
if (currentMillis - PeriodStart >= LED_OFF_Period)
{
Serial.println("Wait-Time with LED OFF is over");
Serial.println("I'm ready for a new buttonpress");
myStateVar = waitForButtonPressed;
}
break;
}
} // end of function manageStates()
void setup()
{
Serial.begin(115200);
Serial.println("Setup-Start");
PrintFileNameDateTime();
pinMode(buttonPin, INPUT_PULLUP);
digitalWrite(onBoard_LED, LOW);
pinMode(onBoard_LED, OUTPUT);
myStateVar = waitForButtonPressed;
Serial.println("press button at least for two seconds to start the demo");
}
// parts of the program that belong to one functional unit are coded in their
// own function where each function has a self-explaining name
// This makes it much easier to test code and keep an overview
// with the growing of the code
void loop()
{
countLoops++;
slowDownSerialOutPut();
printStateInfo();
manageStates();
}
// explanation:
// dear newbee,
// the program expects a button connected to IO-pin 12
// and does switch on / off IO-pin 13 which is connected to the onboard-LED
// on an arduino uno-board
// a state-machine is a medium advanced programming-technique.
// So if you hardly know what a constant, a variable, an if-condition is
// understanding this program can be ambitious and hard
// in this case I recommend learning the fundamental basics first
// I'm very interested in your experience if this program is easy to understand
// or hard to understand
// so whatever questions you have about this code your questions from YOU are a
// very welcomed feedback to improve the understandability
// the main code has around 100 lines. So indeed it will take some time to understand it.
// for watching what the program-FLOW does start the serial monitor and adjust baud to
// 115200 baud
// after 20 seconds deactivate "autoscroll" in the serial monitor or unplug your microcontroller-board
// and then scroll back the serial monitor and start reading the lines
// to understand what the program does.
// again: If you have any questions feel free to ask the questions in the Arduino-Forum
// I'm very interested in your questions because your questions are feedback what is still
// difficult to understand.
Of course I do not reommend to program this way. Some newbees work this way: where do I find a code with the same keywords as my code needs? OK here is one lets take a look into it and lets do a quick shot into the fog to modify it to my needs.
bad approach but I see it again and again.
Such newbees tend to keep everything they don't need to touch as it is.
"If I change it it might not work anymore".
Even if this means that names and whatever do not fit any longer.
To make it easier for them to understand what can be changed and what must e kept I would rename the character-constants
const char *StateNames[] = // Just for display
{
"wait for Button to be pressed ",
"switch the LED on ",
"wait Until LED-ON Period ss Over ",
"switch OFF LED ",
"wait UntilLED-OFF-Period is Over "
};
This shows another somehow advanced concept how to make code more compact but levers the grade of difficulty to understand what the code does. As it is a "helper-function" it could be seen as don't worry about it is a helper-function
best regards Stefan