Following advice here on the forum, I have implemented a FSM in my code for controlling a dog flap in an external door. Here is the logical flow chart I drew to try to design the system:-
Here is the code I am using (for Node MCU ESP8266). WARNING - I am new to coding! I can't figure out why the system will correctly check if the Master Switch and Door Sensor state when first booted. However, once passed these states, Ready State or Monitor Flap Open, , I can turn on and off the master switch and open the door, without it changing back to these initial states. Do I have to check inside each state for all inputs, which seems to defeat the purpose of the Switch Case statement. Also, I cant quite discern the logic of including breaks after each case. Any help appreciated!
I have to split code into next post, as it's too long.
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
const char* ssid = "XXXXXXXXXXXXXXXXXX";
const char* password = "YYYYYYYYYYYYYYYYYY";
const unsigned long flapOpenTime = 30000; //in addition to timing of piR pot
unsigned long previousTime = 0; //priortime flap opened
unsigned long priorTime = 0; // prior time for WiFi timeout
const long timeoutTime = 2000; // Define timeout time in milliseconds (example: 2000ms = 2s)
const int relay2 = D1; //LOW state switches relay to linear actuator to close flap GPIO 5
const int relay1 = D2; //LOW state switches relay to linear actuator to open flap GPIO 4
const int pIRoUT = D5; // HIGH signal when motion detected GPIO 14
const int pIRiN = D6; // HIGH signal when motion detected GPIO 12
const int masterSwitch = D3; //manual switch to close flap- GPIO 0 - boot fails if pulled low - so should be switch off when starting system
const int doorSensor = D7; // reed switch to check if door is closed before starting system GPIO 13
String stringState;
String priorStringState;//convert stringState into Char so can display in Serial Monitor
int priorpIRiNStatus;
int priorpIRoUTStatus;
int priorSystemState;
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
void openFlap()
{
digitalWrite(relay1, LOW);// turn relay 1 ON
digitalWrite(relay2, HIGH);// turn relay 2 OFF
}//openFlap()
void closeFlap()
{
digitalWrite(relay1, HIGH);// turn relay 1 OFF
digitalWrite(relay2, LOW);// turn relay 2 ON
}//closeFlap()
enum _state_enum
{
CHECK_MASTER_SWITCH, //manual switch to turn off system
MONITOR_DOOR_OPEN,//If door is open, close flap
CHECK_TIME,//check time of day to decide which action to take
DAY_TIME,//Day time dogs can come in and go out
BED_TIME, //allow 1 hour between 2300hrs and midnight for dogs to come in before system turns off for night
NIGHT_TIME, // system closed from midnight to 8 am
READY, // system waiting for valid activity from PIR sensors
CMD_OPEN_FLAP, //transition state when flap is opening
MONITOR_OPEN_FLAP,//make sure flap doesn't close while dog is walking through
CMD_CLOSE_FLAP, //transition state when flap is closing
};
_state_enum systemState = CHECK_MASTER_SWITCH;
void setup() {
closeFlap();
Serial.begin(115200);
// Connect to Wi-Fi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.print ("Connected to: ");
Serial.println(WiFi.localIP());
Serial.println ("Waiting 30 seconds to stabalize PIRs");
pinMode(relay1, OUTPUT);// set pin as output for relay 1
pinMode(relay2, OUTPUT);// set pin as output for relay 2
pinMode(pIRiN, INPUT);
pinMode(pIRoUT, INPUT);
pinMode(doorSensor, INPUT);
pinMode(masterSwitch, INPUT);
// Initialize a NTPClient to get time
timeClient.begin();
// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
timeClient.setTimeOffset(0);
delay(30000); //wait for PIRs to stabalize
}
UKHeliBob:
Why not read the switch states in loop() and only use the values required in the code for a particular state ?
UKHeliBob - Thank you for your reply. I have posted the code, following your comment and if I understood your suggestion correctly, I believe that I am already reading the switch states in the loop. Do I need to read the switch states in every case also? I notice that while I am conditionally serial printing on each change of systemState, some of the variables do not appear to be updating, specifically *masterSwitchStatus *and *doorSensorStatus *
I can't figure out why the system will correctly check if the Master Switch and Door Sensor state when first booted. However, once passed these states, Ready State or Monitor Flap Open, , I can turn on and off the master switch and open the door, without it changing back to these initial states. Do I have to check inside each state for all inputs, which seems to defeat the purpose of the Switch Case statement.
Warning! I am not very good at following other people's code (sometimes I can't follow my own code!!!)
You seem to be confusing states with tests and actions, for example
case CHECK_MASTER_SWITCH:
Checking the master switch is not a state, it's an test. States are things that persist until something changes them, for example your dog flap being closed is a state. You then have to decide what actions have to happen while in the DOGFLAPCLOSED state by doing a test of anything that will move it to a different state. DOGFLAPOPENING is another state.
So,
Inside the DOGFLAPCLOSED state you test for anything that would cause the flap to be opened. When that event happens you start the motors and change the state to DOGFLAPOPENING. In the DOGFLAPOPENING state you test for whatever would cause the flap to stop opening, then stop the flap and move to the next state.
You don't need priorSystemState = systemState; because in each state you test to see if you should move to the next state, and the next state will test for moving to the one after that. You have to work out for each state what needs to be checked and what action to take as a result.
Summary:
States persist for a long time, 'long' being anything longer than it takes loop() to go round once up to as long as you like, years if necessary.
Tests take place in each state to see if an action should be taken and a move to a different state
Actions are the outputs, such as opening the flap. Once an action starts move to a different state.
Apart from any problems with logic there could also be problems with your circuit. How are the inputs wired ? Are there pullup or pulldown resistors in place to keep them at a known state at all times or are they floating at an unknown, maybe HIGH, maybe LOW, maybe changing state ?
I would also advise you to stop using Strings, particularly when you keep changing their value as you do. I suggest that you use an array of string (lowercase s) pointers indexed by the value of the state variable. That way you can simply print the text that you require without setting a variable to a different value
PerryBebbington - Thank you for your response. That does clarify matters for me. I had thought that if the
Master Switch was off, then that would be an Off state and same for if the door (not flap) was open, that too
would be a state. Potentially, they can persist for quite an extended time. But I can see that they are also
conditions which determine other states. Should I also move the Time of Day states into conditions for when
to open the flap?
I will try to recode using this logic and see how that goes.
If the door (not flap) was open, that too would be a state.
I suggest you have 4 states:
Closed
Opening
Open
Closing
So, yes, open is a state.
Should I also move the Time of Day states into conditions for when to open the flap?
Yes, time of day is something that triggers a change or is a condition for opening or not opening the door. Time of day is not a state, it keep changing "Tempus fugit ad reditus umquam".
NB Can you edit your post to use the standard format?
UKHeliBob:
Apart from any problems with logic there could also be problems with your circuit. How are the inputs wired ? Are there pullup or pulldown resistors in place to keep them at a known state at all times or are they floating at an unknown, maybe HIGH, maybe LOW, maybe changing state ?
The PIRS are 3v3 versions and I just simply read in the signal. The Master Switch takes the input Pin to ground as does the door sensor (reed switch). Should I have resisters on these circuits?
UKHeliBob:
I would also advise you to stop using Strings, particularly when you keep changing their value as you do. I suggest that you use an array of string (lowercase s) pointers indexed by the value of the state variable. That way you can simply print the text that you require without setting a variable to a different value
I guess you mean where I am translating the enum value to a string, so I can make sense of the conditional Serialprint output. I had a sense that I was doing this in a clumsy manner, but I don't really know how to implement your suggestion. Would you mind sharing an example of how this is done in an array?
joatmon13:
I can't figure out why the system will correctly check if the Master Switch and Door Sensor state when first booted. However, once passed these states, Ready State or Monitor Flap Open, , I can turn on and off the master
I'm noticing that your flowchart doesn't have a 'begin' state. It's the job of setup() to initialise things, and that's what the 'begin' box is about.
Also, if the master switch is off, there's two flows of execution coming out of it? What? Which does it do - close the flap, or "master switch"? And what's with the two simultaneous flows of execution coming out of "ready"? I mean - I know what you mean, but as a diagram it makes no sense.
Anyway. As for states, the only things that need to be FSM states are those loops where the sketch is waiting for something to happen. Those trapezoidal blocks are your states - note that 'MONITOR OPEN FLAP' also ought to be trapezoidal to keep things consistent.
joatmon13:
Following advice here on the forum, I have implemented a FSM in my code for controlling a dog flap in an external door. Here is the logical flow chart I drew to try to design the system:-
Thank you all for contributions (including Jim's suggestion!). I think I have the flow chart and code fixed. I may have a hardware issue with the relays which sometime output 12v and other times strange 3-7 volts. I decided to place a short 1 second delay between changing relays and it seems to have resolved the issue.
Meanwhile, here is the updated flow chart and code:-
I have implemented the Char Array approach instead of string, which has removed strings and that seems to work.
//Designed for ESP8266 with 2 PIR's, 2 Switches and 2 relays driving a linear actuator
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
const char* ssid = "XXXXXXXXXXXXXXXXXXXXXX";
const char* password = "YYYYYYYYYYYYYYYYYYYYYY";
const unsigned long flapOpenTime = 30000; //in addition to timing of piR pot
unsigned long previousTime = 0; //priortime flap opened
unsigned long priorTime = 0; // prior time for WiFi timeout
const long timeoutTime = 2000; // Define timeout time in milliseconds (example: 2000ms = 2s)
const int relay2 = D1; //LOW state switches relay to linear actuator to close flap GPIO 5
const int relay1 = D2; //LOW state switches relay to linear actuator to open flap GPIO 4
const int pIRoUT = D5; // HIGH signal when motion detected GPIO 14
const int pIRiN = D6; // HIGH signal when motion detected GPIO 12
const int masterSwitch = D3; //manual switch to close flap- GPIO 0 - boot fails if pulled low - so should be switch off when starting system
const int doorSensor = D7; // reed switch to check if door is closed before starting system GPIO 13
int priorpIRiNStatus;
int priorpIRoUTStatus;
int priorSystemState;
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
void openFlap()
{
digitalWrite(relay1, LOW);// turn relay 1 ON
delay (1000);
digitalWrite(relay2, HIGH);// turn relay 2 OFF
}//openFlap()
void closeFlap()
{
digitalWrite(relay1, HIGH);// turn relay 1 OFF
delay (1000);
digitalWrite(relay2, LOW);// turn relay 2 ON
}//closeFlap()
enum _state_enum
{
START,
SYSTEM_READY, //checks door is closed, master switch is on and corrrect time of day
MONITOR_ACTIVITY, // system waiting for valid activity from PIR sensors
CMD_OPEN_FLAP, //transition state when flap is opening
BED_TIME,
MONITOR_OPEN_FLAP,//make sure flap doesn't close while dog is walking through
CMD_CLOSE_FLAP, //transition state when flap is closing
};
_state_enum systemState = START;
const char * messages[] = {
"START",
"SYSTEM_READY",
"MONITOR_ACTIVITY",
"CMD_OPEN_FLAP",
"BED_TIME",
"MONITOR_OPEN_FLAP",
"CMD_CLOSE_FLAP"
};
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.print ("Connected to: ");
Serial.println(WiFi.localIP());
Serial.println (" wait 30 seconds for PIRs to stabalize");
pinMode(relay1, OUTPUT);// set pin as output for relay 1
pinMode(relay2, OUTPUT);// set pin as output for relay 2
pinMode(pIRiN, INPUT_PULLUP);
pinMode(pIRoUT, INPUT_PULLUP);
pinMode(doorSensor, INPUT_PULLUP);
pinMode(masterSwitch, INPUT_PULLUP);
// Initialize a NTPClient to get time
timeClient.begin();
// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT 0 = 0
timeClient.setTimeOffset(0);
closeFlap();
delay(30000); //wait for PIRs to stabalize
}
void loop()
{
timeClient.update();
int currentHour = timeClient.getHours();
int currentMin = timeClient.getMinutes();
int currentSecs = timeClient.getSeconds();
int doorSensorStatus = digitalRead(doorSensor);
int masterSwitchStatus = digitalRead(masterSwitch);
unsigned long currentTime = millis(); // Current time
int pIRiNStatus = digitalRead(pIRiN);
int pIRoUTStatus = digitalRead(pIRoUT);
if (systemState != priorSystemState)
{
Serial.print (F("States - FROM:- "));
Serial.print (messages[priorSystemState]);
Serial.print (F(" TO -> "));
Serial.println (messages[systemState]);
Serial.print (F("doorSensorStatus:- "));
Serial.println (doorSensorStatus);
Serial.print (F("masterSwitchStatus:- "));
Serial.println (masterSwitchStatus);
Serial.print (F("Current Time:- "));
Serial.print (currentHour);
Serial.print (F("H:"));
Serial.print (currentMin);
Serial.print (F("m:"));
Serial.print (currentSecs);
Serial.println (F("s:"));
Serial.print (F("PriorPIR / CurrentPIR - INSIDE "));
Serial.print (priorpIRiNStatus);
Serial.print (F(" / "));
Serial.println (pIRiNStatus);
Serial.print (F("PriorPIR / CurrentPIR - OUTSIDE "));
Serial.print (priorpIRoUTStatus);
Serial.print (F(" / "));
Serial.println (pIRoUTStatus);
Serial.print (F("Relay 1 / Relay 2:- "));
Serial.print (digitalRead(relay1));
Serial.print (F(" / "));
Serial.println (digitalRead(relay2));
Serial.print ("Flap Elapsed Time: ");
Serial.println (currentTime - previousTime);
Serial.println (F("***********************"));
}
switch (systemState)
{
//Check that system is switched on
case START:
if(masterSwitchStatus == 0 && doorSensorStatus == 0)
{
systemState = SYSTEM_READY;
}
else systemState = START;
break;
case SYSTEM_READY:
if(masterSwitchStatus == 0 && doorSensorStatus == 0
&& currentHour >= 8 && currentHour <= 23)
{
systemState = MONITOR_ACTIVITY;
}
else systemState = START;
break;
case MONITOR_ACTIVITY: // If either PIR sensor went from LOW to HIGH then open the flap.
if ((pIRiNStatus != priorpIRiNStatus && pIRiNStatus == 1)
||
(pIRoUTStatus != priorpIRoUTStatus && pIRoUTStatus == 1))
{
systemState = CMD_OPEN_FLAP;
previousTime = currentTime;
}
priorpIRiNStatus = pIRiNStatus; //is this necessary here?
priorpIRoUTStatus = pIRoUTStatus; //is this necessary here?S
break;
case BED_TIME: // If OUTSIDE PIR sensor went from LOW to HIGH then open the flap.
//Leeway time to let the dogs in ONLY, but does not allow them out after 11pm.
if ((pIRoUTStatus != priorpIRoUTStatus && pIRoUTStatus == 1)
&& (currentHour >= 23 && currentHour <= 0))
{
systemState = CMD_OPEN_FLAP;
previousTime = currentTime;
}
priorpIRiNStatus = pIRiNStatus;
priorpIRoUTStatus = pIRoUTStatus;
case CMD_OPEN_FLAP:
priorSystemState = systemState;
openFlap();
systemState = MONITOR_OPEN_FLAP;
break;
case MONITOR_OPEN_FLAP:
priorSystemState = systemState;
if (((pIRiNStatus == 0 && pIRoUTStatus == 0) && (currentTime - previousTime >= flapOpenTime))
|| masterSwitchStatus == 1
|| doorSensorStatus == 1)
{
systemState = CMD_CLOSE_FLAP;
}
break;
case CMD_CLOSE_FLAP:
priorSystemState = systemState;
closeFlap();
systemState = START;
break;
priorpIRiNStatus = pIRiNStatus;
priorpIRoUTStatus = pIRoUTStatus;
}
}